if (!gtfs) {
  var gtfs = new Object();
}

(function(gtfs) {
  var INFINITY = 99999999999;
  function windowHeight() {
    // Standard browsers (Mozilla, Safari, etc.)
    if (self.innerHeight)
      return self.innerHeight;
    // IE 6
    if (document.documentElement && document.documentElement.clientHeight)
      return document.documentElement.clientHeight;
    // IE 5
    if (document.body)
      return document.body.clientHeight;
    // Just in case.
    return 0;
  }

  /**
   * Load the driving direction's polyline with the given direction
   * query, and invokes the callback when direction is loaded.
   * @param {String} query String of GDirections' query
   * @param {Object} self The pointer to this of the callback
   * @param {Function} callback Function invoked after the direction is loaded.
   */
  var getDirPolyline = function(query, self, callback) {
    var dirFailureTime = 0;
    var dirTimer = null;
    var DIRECTION_MAX_RETRIES = 20;
    var DIRECTION_INTERVAL = 300;
    var ret = false;

    var clearTimer = function() {
      if (dirTimer) {
        clearTimeout(dirTimer);
        dirTimer = null;
      }
    }

    var dir = new GDirections();
    self = self || this;
    var onDirSuccess = function() {
      if (ret) {
        return;
      }
      clearTimer();
      var pol = dir.getPolyline();
      ret = true;
      callback.call(self, pol);
    };

    var onDirFailure = function() {
      if (ret) {
        return;
      }
      clearTimer();
      if (dirFailureTime < DIRECTION_MAX_RETRIES) {
        dirTimer = setTimeout(onDirTimeOut, 
          DIRECTION_INTERVAL * (dirFailureTime + 1));
        dirFailureTime++;
        return;
      }

      var error = dir.getStatus();
      ret = true;
      callback.call(self, null, error);
    };

    var onDirTimeOut = function() {
      if (ret) {
        return;
      }
      clearTimer();
      if (dirFailureTime > DIRECTION_MAX_RETRIES) {
        onDirFailure();
        return;
      }
      dir.load(query, {
        getPolyline: true
      });
      dirTimer = setTimeout(onDirTimeOut,
        DIRECTION_INTERVAL * (dirFailureTime + 1));
      dirFailureTime++;
    }

    dirTimer = setTimeout(onDirTimeOut, DIRECTION_INTERVAL);
    GEvent.addListener(dir, 'load', onDirSuccess);
    GEvent.addListener(dir, 'error', onDirFailure);

    dir.load(query, {
      getPolyline: true
    });
  };

  /**
   * Get a formatted query string from a GLatLng object
   * @param {GLatLng} point GLatLng object
   * @return {String} GDirections query string
   */
  var getPoint = function(point) {
    return point.lat().toFixed(9) + ',' + point.lng().toFixed(9);
  };

  // Independent Functions
  var $ = function(id) {
    return document.getElementById(id);
  };

  var $C = function(tagName) {
    return document.createElement(tagName);
  };

  var $T = function(text) {
    return document.createTextNode(text);
  };

  /**
   * List of Trips fetched
   * @type {Obejct}
   */
  var tripsFetched = {};

  /**
   * List of Stops fetched
   * @type {Obejct}
   */
  var stopsFetched = {};

  /**
   * List of Stops for corresponding Shapes
   * @type {Obejct}
   */
  var stopsOfShape = {};

  /**
   * List of Shapes fetched
   * @type {Obejct}
   */
  var shapesFetched = {};

  /**
   * Show stops in bounds or not
   * @type {Boolean}
   */
  var showStopsInBounds = false;

  /**
   * Mouse point position
   * @type {GLatLng}
   */
  var mousePos = null;

  /**
   * Enable Leg editing
   * @type {Boolean}
   */
  var legEditable = false;

  /**
   * Enable Stop editing
   * @type {Boolean}
   */
  var stopEditable = false;

  /**
   * Enable Leg reload on Stop movements
   * @type {Boolean}
   */
  var withLegReload = true;

  /**
   * Inside adding stops mode
   * @type {Boolean}
   */
  var addStopMode = false;

  /**
   * Is the exporting in progress
   * @type {Boolean}
   */
  var isExporting = false;

  /**
   * List of actions recorded
   * Basic structure of the recorded objects will be like the following:
   * {
   *   // Type of handlers
   *   type: 'stop',
   *
   *   // Type of actions
   *   act: 'delete',
   *
   *   // Action related objects and values
   *   obj: {
   *     stop: stop,
   *     stoptime: stoptime,
   *     trip: trip,
   *     route: route,
   *     sequence: sequence
   *   }
   * }
   *
   * @type {Array.<Object>}
   */
  var actionList = [];

  /**
   * A class of pattern, handles pattern related operations.
   * @constructor
   */
  function Pattern() {

  }

  /**
   * Handler for deleting stop from a pattern
   * @param {Object} act Infomation of an action
   * @param {Function} cb Callback function
   */
  Pattern.handleDeleteStop = function(act, cb) {
    var stoptime = act.obj.stoptime;
    stoptime.saveChange(cb);
  }

  /**
   * Handler for all pattern actions
   * @param {Object} act Infomation of an action
   * @param {Function} cb Callback function
   */
  Pattern.handleAct = function(act, cb) {
    switch(act.act) {
      case 'deletestoptime':
        Pattern.handleDeleteStop(act, cb);
        break;
    }
  }

  /**
   * Find if the action is registered
   * @param {String} type Type of the action
   * @param {String} act Act of the action
   * @param {String} obj Attribute of the action
   * @param {String} value Value of the attribute
   */
  var actionFind = function(type, act, obj, value) {
    var len = actionList.length;
    for (var i = 0; i < len; i ++) {
      var al = actionList[i];
      if (al && (al.type == type) && (al.act == act) && (al.obj[obj] == value)) {
        return true;
      }
    }
    return false;
  };

  /**
   * Save all changes
   * @param {Function} cb Callback function
   */
  var saveActions = function(cb) {
    var len = actionList.length;
    var idx = 0;
    var saveStep = function() {
      if (idx >= len) {
        if (cb) {
          cb();
        }
        actionList = [];
        return;
      }
      var act = actionList[idx];
      idx++;
      switch (act.type) {
        case 'stop':
          Stop.handleAct(act, saveStep);
          break;
        case 'pattern':
          Pattern.handleAct(act, saveStep);
          break;
        case 'shape':
          Shape.handleAct(act, saveStep);
          break;
      }
    };
    saveStep();
  };

  /**
   * A wrapper class of the select component.
   * It creates a select element of DOM, initializes select options
   * with given names and values, sets the default value and index.
   *
   * @param {Array.<string>} vals An array of values for Options
   * @param {Array.<string>} names An array of names for Options
   * @param {string} defVal Default value for selectedIndex
   */
  var Select = function(vals, names, defVal) {
    var select = $C('select');
    this.elem_ = select;
    for (var i = 0; i < vals.length; i++) {
      select.options[select.options.length] = new Option(
          names[i],
          vals[i],
          defVal == vals[i],
          defVal == vals[i]);
    }
    select.selectedIndex = select.selectedIndex || 0;
  };

  /**
   * Add an option to select
   *
   * @param {String} val  Value of Option
   * @param {String} name Name of Option
   * @param {String} defVal Default value for selectedIndex
   */
  Select.prototype.add = function(val, name, defVal) {
    this.elem_.options.push(new Option(
        name,
        val,
        defVal == val,
        defVal == val));
  };

  /**
   * Remove an option from select
   *
   * @param {Number} idx Index to be removed
   */
  Select.prototype.remove = function(idx) {
    this.elem_.options.remove(idx);
  };

  /**
   * Set an option by assining a value and a name
   *
   * @param {Number} idx Index to be set
   * @param {String} val  Value of Option
   * @param {String} name Name of Option
   */
  Select.prototype.set = function(idx, val, name) {
    this.elem_.options[idx].value = val;
    this.elem_.options[idx].text = name;
  };

  /**
   * Get element from select
   * @return {HTMLElement} Select node
   */
  Select.prototype.getElement = function() {
    return this.elem_;
  };

  /**
   * Fetcher is a class for fetching data from the server side
   * @constructor
   */
  var Fetcher = function() {
  };

  /**
   * A function for asynchronous actions which fetch strings in json
   * from the server side which has the combinated data from objects
   * and database.
   * @param {Object} self The pointer to this of the callback
   * @param {Function} callback A function invoked on the end of the action
   * @return {Function} Function having self and callback information
   */
  Fetcher.prototype.cbFunc = function(self, callback) {
    return function(data, responseCode) {
      if (responseCode != 200) {
        if (callback)
          callback.call(self, data);
        return;
      }
      data = eval(data);
      if (callback)
        callback.call(self, data);
    };
  };

  /**
   * Export GTFS Data from database into zip files
   *
   * @param {Object} self The pointer to this of the callback
   * @param {Function} callback A function invoked at the
   *                            end of the action
   */
  Fetcher.prototype.exportGTFSData = function(self, callback) {
    var url = '/json/gtfsexport';
    GDownloadUrl(url, this.cbFunc(self, callback));
  };

  /**
   * Fetch agency infomation from server side
   *
   * @param {Object} self The pointer to this of the callback
   * @param {Function} callback A function invoked at the
   *                            end of the action
   */
  Fetcher.prototype.getAgencies = function(self, callback) {
    var url = '/json/agency';
    GDownloadUrl(url, this.cbFunc(self, callback));
  };

  /**
   * Fetch route infomation from server side by agency id
   *
   * @param {String} agencyId Agency ID of routes to be fetched
   * @param {Object} self The pointer to this of the callback
   * @param {Function} callback A function invoked at the
   *                            end of the action
   */
  Fetcher.prototype.getRoutes = function(agencyId, self, callback) {
    var url = '/json/routesbyaid?aid=' + encodeURIComponent(agencyId);
    GDownloadUrl(url, this.cbFunc(self, callback));
  };

  /**
   * Fetch trip infomation from server side by route id
   *
   * @param {String} routeId Route ID of trips to be fetched
   * @param {Object} self The pointer to this of the callback
   * @param {Function} callback A function invoked at the
   *                            end of the action
   */
  Fetcher.prototype.getTrips = function(routeId, self, callback) {
    var url = '/json/tripsbyrid?rid=' + encodeURIComponent(routeId);
    GDownloadUrl(url, this.cbFunc(self, callback));
  };

  /**
   * Fetch Stoptime infomation from server side by trip id
   *
   * @param {String} tripId Trip ID of stoptimes to be fetched
   * @param {Object} self The pointer to this of the callback
   * @param {Function} callback A function invoked at the
   *                            end of the action
   */
  Fetcher.prototype.getStoptimes = function(tripId, self, callback) {
    var url = '/json/stoptimesbytid?tid=' + encodeURIComponent(tripId);
    GDownloadUrl(url, this.cbFunc(self, callback));
  };

  /**
   * Query server to see if the shape is existed, callback will be
   * passed an array where it contains only one element, and the element
   * has only two correct values:
   *   0 for non-existence
   *   1 for existence.
   *
   * @param {String} shapeId Shape ID
   * @param {Object} self The pointer to this of the callback
   * @param {Function} callback A function invoked at the
   *                            end of the action
   */
  Fetcher.prototype.doesShapeExist = function(shapeId, self, callback) {
    var url = '/json/shapeexist?id=' + encodeURIComponent(shapeId);
    GDownloadUrl(url, this.cbFunc(self, callback));
  };

  /**
   * Fetch Shape infomation from server side by trip id
   *
   * @param {String} tripId Trip ID of shape data to be fetched
   * @param {Object} self The pointer to this of the callback
   * @param {Function} callback A function invoked at the
   *                            end of the action
   */
  Fetcher.prototype.getShapes = function(tripId, self, callback) {
    var url = '/json/shapesbytid?tid=' + encodeURIComponent(tripId);
    GDownloadUrl(url, this.cbFunc(self, callback));
  };

  /**
   * Fetch Stops in Bounds
   *
   * @param {GLatLngBounds} bounds Map bounds
   * @param {Object} self The pointer to this of the callback
   * @param {Function} callback A function invoked at the
   *                            end of the action
   */
  Fetcher.prototype.getStopsInBounds = function(bounds, self, callback) {
    var params = {};
    params['n'] = bounds.getNorthEast().lat();
    params['e'] = bounds.getNorthEast().lng();
    params['s'] = bounds.getSouthWest().lat();
    params['w'] = bounds.getSouthWest().lng();
    params['limit'] = 50;

    var url = '/json/boundboxstops?' + this.kv2URI(params);
    GDownloadUrl(url, this.cbFunc(self, callback));
  };

  /**
   * Fetch Stop infomation from server side by trip id
   *
   * @param {String} stopId Stop ID
   * @param {Object} self The pointer to this of the callback
   * @param {Function} callback A function invoked at the
   *                            end of the action
   */
  Fetcher.prototype.getStopInfo = function(stopId, self, callback) {
    var params = {};
    params['stop'] = stopId;
    params['time'] = 0;

    var url = '/json/stoptrips?' + this.kv2URI(params);
    GDownloadUrl(url, this.cbFunc(self, callback));
  };

  /**
   * Fetch Stop infomation from server side by stop id
   *
   * @param {String} stopId Stop ID
   * @param {Object} self The pointer to this of the callback
   * @param {Function} callback A function invoked at the
   *                            end of the action
   */
  Fetcher.prototype.getStopEditInfo = function(stopId, self, callback) {
    var url = '/json/stopinfo?stopId=' + encodeURIComponent(stopId);
    GDownloadUrl(url, this.cbFunc(self, callback));
  };

  /**
   * Save Shape infomation to server side
   *
   * @param {String} postBody Post data string
   * @param {Object} self The pointer to this of the callback
   * @param {Function} callback A function invoked at the
   *                            end of the action
   */
  Fetcher.prototype.saveShape = function(postBody, self, callback) {
    var url = '/post/shape';
    GDownloadUrl(url, this.cbFunc(self, callback), postBody);
  };

  /**
   * Save Stop infomation to server side
   *
   * @param {String} kvObj Key value object
   * @param {Object} self The pointer to this of the callback
   * @param {Function} callback A function invoked at the
   *                            end of the action
   */
  Fetcher.prototype.updateStop = function(kvObj, self, callback) {
    var url = '/post/stop';
    GDownloadUrl(url, this.cbFunc(self, callback), this.kv2URI(kvObj));
  };

  /**
   * Update info for certain pattern
   *
   * @param {String} kvObj Key value object
   * @param {Object} self The pointer to this of the callback
   * @param {Function} callback A function invoked at the
   *                            end of the action
   */
  Fetcher.prototype.updatePattern = function(kvObj, self, callback) {
    var url = '/post/pattern';
    GDownloadUrl(url, this.cbFunc(self, callback), this.kv2URI(kvObj));
  };

  /**
   * Parameterize an Object to a string in the URI format
   *
   * @param {Object} kvObj The key-value object
   * @param {Object} kvSep The key-value separator
   * @param {Object} entrySep The entry separator
   * @return {String} URL string
   */
  Fetcher.prototype.kv2URI = function(kvObj, kvSep, entrySep) {
    var pairs = [];
    kvSep = kvSep || '=';
    entrySep = entrySep || '&';
    for (key in kvObj) {
      pairs.push(key + kvSep + encodeURIComponent(kvObj[key]));
    }
    return pairs.join(entrySep);
  };

  /**
   * Toolbar for trip only, changes on every click on the nodes
   * of the trip sections.
   * @constructor
   */
  InnerToolbar = function() {
  };

  InnerToolbar.prototype = new GControl();

  /**
   * Create a new checkbox under the given parent node, and return it.
   * @param {HTMLElement} pNode Node the newly created checkbox to be attached
   * @param {Boolean} checked Default check value for the created checkbox
   * @param {String} text Tailing infomation
   * @return {HTMLElement} New created checkbox node
   */
  InnerToolbar.prototype.createCheckBox = function(pNode, checked, text) {
    var node = $C('input');
    node.type = 'checkbox';
    node.checked = checked;
    pNode.appendChild(node);
    pNode.appendChild($T(text));
    return node;
  };

  /**
   * Create a new button under the given parent node, and return it.
   * @param {HTMLElement} pNode Node the newly created checkbox to be attached
   * @param {String} text Tailing infomation
   * @return {HTMLElement} New created button
   */
  InnerToolbar.prototype.createButton = function(pNode, text) {
    var anchor = $C('a');
    anchor.href = 'javascript:void(0)';
    anchor.innerHTML = text;
    anchor.color = '#ccc';
    anchor.cursor = 'pointer';
    pNode.appendChild($T(' ['));
    pNode.appendChild(anchor);
    pNode.appendChild($T('] '));
    return anchor;
  };

  /**
   * Handler for inbounds trip switching
   */
  InnerToolbar.prototype.onSwitchTrips = function() {
    map.closeInfoWindow();
    showStopsInBounds = this.switchTrips.checked;
    for (stopId in stopsFetched) {
      var stop = stopsFetched[stopId];
      if (!stop.isTripSelected) {
        showStopsInBounds ? stop.show() : stop.clear();
      }
    }
  };

  /**
   * Handler for the change of leg editing options
   */
  InnerToolbar.prototype.onLegOptionChange = function() {
    map.closeInfoWindow();
    legEditable = this.legEditable.checked;
  };

  /**
   * Handler for the change of leg reload options
   */
  InnerToolbar.prototype.onLegReloadOptionChange = function() {
    map.closeInfoWindow();
    withLegReload = this.withLegReload.checked;
  };

  /**
   * Set status to add stop or not
   */
  InnerToolbar.prototype.onAddStopChange = function() {
    map.closeInfoWindow();
    addStopMode = this.addStop.checked;
    if (addStopMode) {
      map.addOverlay(Stop.addStopCursor);
    } else {
      map.removeOverlay(Stop.addStopCursor);
      if (Trip.nearestLeg) {
        Trip.nearestLeg.hide();
        Trip.nearestLeg.show();
      }
    }
  };

  /**
   * Handler for the change of stop editing options
   */
  InnerToolbar.prototype.onStopOptionChange = function() {
    map.closeInfoWindow();
    stopEditable = this.stopEditable.checked;
    Trip.current.enableStopEdit(stopEditable);
    if (stopEditable) {
      this.stopEditablePanel.style.display = '';
      addStopMode = this.addStop.checked;
    } else {
      this.stopEditablePanel.style.display = 'none';
      addStopMode = false;
      map.removeOverlay(Stop.addStopCursor);
      if (Trip.nearestLeg) {
        Trip.nearestLeg.hide();
        Trip.nearestLeg.show();
      }
    }
  };

  /**
   * Initialize Trip tool bar
   * @param {GMap2} map tool bar container map
   */
  InnerToolbar.prototype.initialize = function(map) {
    var container = $C('p');
    container.className = 'toolbar';
    var checkOptions = $C('span');
    container.appendChild(checkOptions);

    this.switchTrips = this.createCheckBox(checkOptions, showStopsInBounds,
        'Show all stops');
    GEvent.bindDom(this.switchTrips, 'click', this, this.onSwitchTrips);

    this.legEditable = this.createCheckBox(checkOptions, legEditable,
        'Edit Leg');
    GEvent.bindDom(this.legEditable, 'click', this, this.onLegOptionChange);

    this.stopEditable = this.createCheckBox(checkOptions, stopEditable,
        'Edit Stop');
    GEvent.bindDom(this.stopEditable, 'click', this, this.onStopOptionChange);

    this.checkOptions = checkOptions;

    this.stopEditablePanel = $C('span');
    this.addStop = $C('input');
    this.addStop.type = 'checkbox';
    this.addStop.id = 'addStop';
    GEvent.bindDom(this.addStop, 'click', this, this.onAddStopChange);
    this.stopEditablePanel.appendChild(this.addStop);
    if (!stopEditable) {
      this.stopEditablePanel.style.display = 'none';
    }
    this.stopEditablePanel.className = 'addStopPanel';
    this.stopEditablePanel.appendChild($T('Add Stop'));
    this.checkOptions.appendChild(this.stopEditablePanel);

    /*
    this.withLegReload = this.createCheckBox(checkOptions, withLegReload,
        'Leg Reload');
    GEvent.bindDom(this.withLegReload, 'click', this,
      this.onLegReloadOptionChange);
    */
    var actionOptions = $C('span');
    container.appendChild(actionOptions);

    this.undoButton = this.createButton(actionOptions, 'Undo');
    container.appendChild($T(' '));
    this.undoButton.className = 'disabled';

    this.redoButton = this.createButton(actionOptions, 'Redo');
    container.appendChild($T(' '));
    this.redoButton.className = 'disabled';

    this.verifyButton = this.createButton(actionOptions, 'Generate Shape');
    container.appendChild($T(' '));

    var hint = $C('span');
    hint.id = 'trip-hint';
    this.hint = hint;

    container.appendChild(hint);
    map.getContainer().appendChild(container);
    return container;
  };

  /**
   * By default, the control will appear in the top left corner of the
   * map with 7 pixels of padding.
   * @override
   */
  InnerToolbar.prototype.getDefaultPosition = function() {
    return new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(68, 7));
  };


  /**
   * Base is a base class for gtfs's data classes.
   * It contains basic attributes and functions commonly needed
   * @constructor
   */
  var Base = function() {
    /**
     * Observers of events
     * @type {Object}
     */
    this.observers = {};

    /**
     * Condition to determine if the related data is fetched
     * @type {Boolean}
     */
    this.retrieved = false;

    /**
     * Condition to determine if the object is modified
     * @type {Boolean}
     */
    this.modified = false;

    /**
     * Tag to see if the object is in fetching status
     * @type {Boolean}
     */
    this.inAsync = false;

  };

  Base.prototype.fetcher = new Fetcher();

  /**
   * A function for classes to register an asynchronous
   * action's callback
   * @param {Function} callback Callback for successful invocation
   * @param {Function} callbackOnError Callback for failed invocation
   * @return {Function} Function having both this and callbacks information
   */
  Base.prototype.cbFunc = function(callback, callbackOnError) {
    var self = this;
    return function(isSuccess, data) {
      if (!isSuccess) {
        if (callbackOnError)
          callbackOnError.call(self, data);
        return;
      }
      callback.call(self, data);
    };
  };

  /**
   * Add listeners to the object
   * @param {String} event Event to be observed
   * @param {Object} obj Object in whose scope to call the listener function.
   * @param {Function} func Function to be invoked on event
   * @return {Object} Handler for events
   */
  Base.prototype.addListener = function(event, obj, func) {
    if (!this.observers[event]) {
      this.observers[event] = [];
    }
    var handler = {
      event: event,
      object: obj,
      func: func
    };
    this.observers[event].push(handler);
    return handler;
  };

  /**
   * Remove the listener from the object
   * @param {Object} o Handler to be removed
   * @return {Boolean} Success of the action
   */
  Base.prototype.removeListener = function(o) {
    if (!this.observers[o.event] || ! this.observers[o.event].length) {
      return false;
    }
    for (var i = 0; i < this.observers[o.event].length; i++) {
      if (o == this.observers[o.event][i]) {
        this.observers[o.event].splice(i, 1);
        return true;
      }
    }
    return false;
  };

  /**
   * Event invoker
   * @param {Object} e Event to be triggered
   * @param {Object} data Data to be feeded to handler
   *
   */
  Base.prototype.triggerEvent = function(e, data) {
    if (!this.observers[e] || ! this.observers[e].length) return;
    for (var i = 0; i < this.observers[e].length; i++) {
      var o = this.observers[e][i];
      o.func.call(o.object, data);
    }
  };

  /**
   * Abstract function for GTFS Objects to overriding
   * to get Object specific serialize result
   */
  Base.prototype.serialize = function() {};


  /**
   * Manager is a class for introducing and manipulating of gtfs's
   * data. It initializes the gtfs's data information of the
   * schedule editor.
   * @constructor
   */
  var Manager = function() {
    Base.call(this);
    /**
     * All agencies from the GTFS feed
     * @type {Array}
     */
    this.agencies = [];
  };
  Manager.prototype = new Base();

  /**
   * Help function to generate all shapes of items, and all items should
   * have member function named generateAllShapes and have one parameter
   * can be passed a callback function
   *
   * @param {Array} items Array of Objects
   * @param {Function} callback A function invoked at the
   *                            end of the action
   */
  var generateAll = function(items, callback) {
    var idx = 0;
    var generateChildShape = function() {
      if (idx >= items.length) {
        if (callback) {
          callback();
        }
        return;
      }
      var item = items[idx];
      idx++;
      if (item) {
        item.generateAllShapes(generateChildShape);
      } else {
        generateChildShape();
      }
    }
    generateChildShape();
  }

  /**
   * Generate route shapes for all the agencies of the manager.
   * @param {Function} callback A function invoked at the
   *                            end of the action
   */
  Manager.prototype.generateAllShapes = function(callback) {
    var self = this;

    this.getAgencies(function() {
      generateAll(self.agencies, callback);
    });
  }
  /**
   * Get agency data from server and initialize Agency Objects
   *
   * @param {Function} callback A function invoked on the
   *                            end of the action
   */
  Manager.prototype.getAgencies = function(callback) {
    if (this.inAsync) return;
    if (this.retrieved) {
      if (callback) callback();
      return;
    }
    var self = this;
    this.fetcher.getAgencies(this, function(data) {
      self.initAgency(data, callback);
      this.retrieved = true;
      this.inAsync = false;
    });
  };

  /**
   * Get agency data from server and initialize Agency Objects
   *
   * @param {Array} data Array of agency data
   * @param {Function} callback A function invoked on the
   *                            end of the action
   */
  Manager.prototype.initAgency = function(data, callback) {
    //initialize All for fetching all routes
    var agency = new Agency();
    agency.id = '0';
    agency.name = 'All';
    this.agencies.push(agency);
    for (var i = 0; i < data.length; i++) {
      var iterator = data[i];
      agency = new Agency();
      var attrs = ['id', 'name', 'url', 'phone', 'timezone', 'lang'];
      for (var j = 0, attr; attr = attrs[j]; j++) {
        agency[attr] = iterator[attr];
      }
      this.agencies.push(agency);
    }
    if (callback) callback();
  };

  /**
   * Show agency's contents in editor
   */
  Manager.prototype.showAgencies = function() {
    var container = $('agencies');
    var vals = [];
    var names = [];
    for (var i = 0; i < this.agencies.length; i++) {
      var agency = this.agencies[i];
      vals.push(agency.id);
      names.push(agency.name);
    }

    if (!vals.length) return;
    var select = new Select(vals, names, vals[0]);
    container.appendChild(select.getElement());
    this.select = select;
    GEvent.bindDom(select.getElement(), 'change', this, this.onSelect);
    this.onSelect();
  };

  /**
   * Event handler for agency selection
   */
  Manager.prototype.onSelect = function() {
    var idx = this.select.getElement().selectedIndex || 0;
    var agency = this.agencies[idx];
    agency.getRoutes(function() {
      agency.showRoutes();
    });
  };

  /**
   * Agency is a class representing a GTFS agency.
   * @constructor
   */
  function Agency() {
    Base.call(this);

    /**
     * All routes of the agency
     * @type {Array}
     */
    this.routes = [];

    /**
     * agency_id
     * @type {String}
     */
    this.id = '';

    /**
     * agency_name
     * @type {String}
     */
    this.name = '';

    /**
     * agency_url
     * @type {String}
     */
    this.url = '';

    /**
     * agency_timezone
     * @type {String}
     */
    this.timeZone = '';


    /**
     * agency_lang
     * @type {String}
     */
    this.lang = '';

    /**
     * agency_phone
     * @type {String}
     */
    this.phone = '';

    /**
     * Currently selected Route object
     * @type {Route}
     */
    this.curRoute_ = null;
  }

  Agency.prototype = new Base();

  /**
   * Generate All shapes of the object
   * @param {Function} callback A function invoked at the
   *                            end of the action
   */
  Agency.prototype.generateAllShapes = function(callback) {
    var self = this;
    this.getRoutes(function() {
        self.showRoutes();
        generateAll(self.routes, callback);
    });
  }
  /**
   * Get routes by agency id
   * @param {Function} callback Callback invoked if retrieved
   */
  Agency.prototype.getRoutes = function(callback) {
    if (this.retrieved) {
      if (callback) callback();
      return;
    }
    if (this.inAsync) return;
    this.fetcher.getRoutes(this.id, this, function(data) {
      this.parseRoutes_(data);
      if (callback) callback();
      this.retrieved = true;
      this.inAsync = false;
    });
  };

  /**
   * Parse routes data into Route objects.
   * @param {Array} data Data retrieved from server to be parsed
   * @private
   */
  Agency.prototype.parseRoutes_ = function(data) {
    for (var i = 0; i < data.length; i++) {
      var route = new Route();
      for (var o in data[i]) {
        route[o] = data[i][o];
      }
      route['name'] = route['longName'] || route['shortName'];
      route.addListener('click', this, this.onRouteSelected);
      this.routes.push(route);
    }
  };


  /**
   * Show routes
   */
  Agency.prototype.showRoutes = function() {
    var routeList = $('routeList');
    routeList.innerHTML = '';
    for (var i = 0; i < this.routes.length; i++) {
      this.routes[i].show(routeList);
    }
  };

  /**
   * Handler for route selected event
   * @param {Object} data Wrapped info from event sender
   */
  Agency.prototype.onRouteSelected = function(data) {
    if (this.curRoute_ && this.curRoute_ != data.route) {
      this.curRoute_.resetDomNode();
    }
    this.curRoute_ = data.route;
  };

  /**
   * Route is a class representing a GTFS route.
   * @constructor
   */
  function Route() {
    Base.call(this);

    /**
     * route_id
     * @type {String}
     */
    this.id = '';

    /**
     * route_short_name
     * @type {String}
     */
    this.shortName = '';

    /**
     * route_long_name
     * @type {String}
     */
    this.longName = '';

    /**
     * route_type
     * @type {String}
     */
    this.type = '';

    /**
     * agency_id
     * @type {String}
     */
    this.agencyId = '';

    /**
     * route_desc
     * @type {String}
     */
    this.desc = '';

    /**
     * route_url
     * @type {String}
     */
    this.url = '';

    /**
     * route_color
     * @type {String}
     */
    this.color = '';

    /**
     * route_text_color
     * @type {String}
     */
    this.textColor = '';

    /**
     * Tag to determine if the route container is created, which can be
     * attached to the node passed for display.
     * @type {Boolean}
     */
    this.drawable = false;

    /**
     * Currently selected Trip object
     * @type {Trip}
     * @private
     */
    this.curTrip_ = null;

  }

  Route.prototype = new Base();


  /**
   * Add environment info of adding stop into Route
   * @param {Number} patternId Pattern ID of trip
   * @param {Stop} stop An Instance of Stop
   * @param {Number} idx Index of the stoptimes where the stop is inserted
   * @param {Number} sequence Sequence of the stoptimes in the trip
   */
  Route.prototype.addPatternStopEnvInfo = function(patternId, stop, idx, sequence) {
    if (!this.addedPatternStops) {
      this.addedPatternStops = {};
    }
    if (!this.addedPatternStops[patternId]) {
      this.addedPatternStops[patternId] = [];
    }
    this.addedPatternStops[patternId].push([stop, idx, sequence]);
  };

  /**
   * Show the Route info in node
   * @param {HTMLElement} node A dom node for Route to manipulate
   */
  Route.prototype.show = function(node) {
    if (!this.drawable) {
      var name = '';
      if (this.shortName) {
        name += '[' + this.shortName + ']';
      }
      if (this.longName) {
        name += this.longName;
      }
      this.nameNode = $C('span');
      this.nameNode.className = 'shortName';
      this.nameNode.appendChild($T(name));

      this.routeNode = $C('div');
      this.routeNode.appendChild(this.nameNode);
      this.routeNode.id = 'route_' + this.id;
      this.routeNode.className = 'routeChoice';
      this.routeNode.title = name;
      GEvent.bindDom(this.routeNode, 'click', this, this.onSelect);

      this.routeContainer = $C('div');
      this.routeContainer.id = 'route_container_' + this.id;
      this.routeContainer.className = 'routeContainer';
      this.routeContainer.appendChild(this.routeNode);

      this.tripSection = $C('div');
      this.tripSection.className = 'tripSection';

      this.routeContainer.appendChild(this.tripSection);
      this.drawable = true;
    }
    node.appendChild(this.routeContainer);
    this.routeNode.className = 'routeChoice';
    this.tripSection.style.display = 'none';
  };

  /**
   * Handler for Route selected
   */
  Route.prototype.onSelect = function() {
    this.tripSection.style.display = '';
    this.routeNode.className = 'routeChoiceSelected';
    this.triggerEvent('click', {
      route: this
    });
    var self = this;
    this.getTrips(function() {
      self.showTrip();
    });
  };

  /**
   * Show default or current trip
   */
  Route.prototype.showTrip = function() {
    if (!this.trips || !this.trips.length) return;
    if (!this.curTrip_) {
      this.curTrip_ = this.trips[0]
    }
    if (this.curTrip_) {
      this.curTrip_.onSelect();
    }
  };

  /**
   * Reset Route node to it's initial state
   */
  Route.prototype.resetDomNode = function() {
    this.tripSection.style.display = 'none';
    this.routeNode.className = 'routeChoice';
  };

  Route.prototype.generateAllShapes = function(callback) {
    var self = this;
    if (this.type != 3) {
      if (callback) callback();
      return;
    }
    this.getTrips(function() {
      var shapeIds = self.shapeIds;
      var idx = 0;
      var generateNextShape = function() {
        if (idx >= shapeIds.length) {
          if (callback) callback();
          return;
        }
        var shapeId = shapeIds[idx];
        if (!shapeId) {
          if (callback) callback();
          return;
        }
        var trips = self.shapes[shapeId];
        idx++;
        // If shape data fetched
        if (!shapesFetched[shapeId]) {
          self.fetcher.doesShapeExist(shapeId, self, function(data) {
            // If shape exists or not
            if (!data[0]) {
              var trip = trips[0];
              trip.getStoptimes(function() {
                // Set no data for the shape
                trip.parseShapes([]);
                trip.verifyMultiPoint(function() {
                  trip.shape.save(generateNextShape);
                });
              });
            } else {
              // Shape exists
              generateNextShape();
            }
          });  // End of self.fetcher.doesShapeExist
        } else {
          // Shape fetched
          generateNextShape();
        }
      }  // End of generateNextShape
      // Start generating
      generateNextShape();
    });  // End of this.getTrips
  }

  /**
   * Get all trips of the route
   * @param {Function} callback Optional callback when fetching finished
   */
  Route.prototype.getTrips = function(callback) {
    if (this.retrieved) {
      if (callback) callback();
      return;
    }
    if (this.inAsync) return;
    this.inAsync = true;
    this.fetcher.getTrips(this.id, this, function(data) {
      this.parseTrips_(data);
      this.initPatternSection_();
      if (callback) callback();
      this.retrieved = true;
      this.inAsync = false;
    });
  };

  /**
   * Parse patterns and trips of the route
   * @param {Array} data Wrapped trip data sorted by patterns
   */
  Route.prototype.parseTrips_ = function(data) {
    var trips = [];
    var shapes = {};
    var shapeIds = [];
    var patterns = data[0];
    this.patterns = {};
    for (var id in patterns) {
      pattern = patterns[id];
      this.patterns[id] = [];
      for (var i = 0; i < pattern.length; i++) {
        var trip = new Trip();
        for (var o in pattern[i]) {
          trip[o] = pattern[i][o];
        }
        if (!shapes[trip.shapeId]) {
          shapes[trip.shapeId] = [];
          shapeIds.push(trip.shapeId);
        }
        shapes[trip.shapeId].push(trip);
        trip.setRoute(this);
        trips.push(trip);
        trip.rid = this.id;
        trip.patternId = id;
        this.patterns[id].push(trip);
        trip.addListener('click', this, this.onTripSelected);
      }
    }
    this.trips = trips;
    this.shapes = shapes;
    this.shapeIds = shapeIds;
  };

  /**
   * Initialize Pattern Section
   * @private
   */
  Route.prototype.initPatternSection_ = function() {
    for (var id in this.patterns) {
      pattern = this.patterns[id];
      var patternNode = $C('div');
      var patternDesc = $C('span');
      patternNode.className = 'patternSection';
      patternDesc.innerHTML = pattern[0].shapeDesc + ', '
        + pattern.length + ' trips: ';
      patternNode.appendChild(patternDesc);
      this.tripSection.appendChild(patternNode);
      this.initTripSection_(pattern, patternNode);
    }
  };

  /**
   * Initialize Pattern Section
   * @param {Array.<Trip>} pattern Array of Trips
   * @param {HTMLElement} patternNode Node for trips to be displayed
   * @private
   */
  Route.prototype.initTripSection_ = function(pattern, patternNode) {
    var toggleButton = null;
    function toggleTrip(val) {
      for (var i = 3, trip; trip = pattern[i]; i++) {
        trip.node.style.display = val;
      }
    }

    function onToggleButtonClick() {
      if (this.hide) {
        toggleTrip('');
        this.innerHTML = '&uArr;';
        this.hide = false;
      } else {
        toggleTrip('none');
        this.innerHTML = '...';
        this.hide = true;
      }
    }

    for (var i = 0, trip; trip = pattern[i]; i++) {
      var node = null;
      if (i < 3) {
        node = trip.createNode();
      } else {
        if (!toggleButton) {
          toggleButton = $C('span');
          toggleButton.innerHTML = '...';
          toggleButton.style.cursor = 'pointer';
          toggleButton.hide = true;
          patternNode.appendChild(toggleButton);
          patternNode.appendChild($C('br'));
          toggleButton.className = 'toggleButton';
          GEvent.bindDom(toggleButton, 'click', toggleButton,
            onToggleButtonClick);
        }
        node = trip.createNode(true);
      }
      patternNode.appendChild(node);
      patternNode.appendChild($T(' '));
    }
  };

  /**
   * Handler for trip to be selected
   * @param {Object} data Event message
   */
  Route.prototype.onTripSelected = function(data) {
    if (this.curTrip_ && this.curTrip_ != data.trip) {
      this.curTrip_.resetDisplay();
    }
    // When different Route selected, Trip.current is different from
    // this.curTrip_
    if (Trip.current && Trip.current != data.trip) {
      Trip.current.resetDisplay();
    }

    this.curTrip_ = data.trip;
    Trip.current = data.trip;
  };

  /**
   * Get Html info of Route
   * @return {String} Route info in Html
   */
  Route.prototype.getInfoHtml = function() {
    var routeAttrPairs = {
      id: 'route_id',
      shortName: 'route_short_name',
      longName: 'route_long_name',
      type: 'route_type',
      agencyId: 'agency_id',
      desc: 'route_desc',
      url: 'route_url',
      color: 'route_color',
      textColor: 'route_text_color'
    };

    var str = '<b>routes.txt</b>:';
    for (var key in routeAttrPairs) {
      str += '&nbsp;&nbsp;<b>' + routeAttrPairs[key] + '</b>: ' + this[key];
    }
    return str;
  };

  /**
   * Trip is a class representing a GTFS trip.
   * @constructor
   */
  function Trip() {
    Base.call(this);
    /**
     * trip_id
     * @type {String}
     */
    this.id = '';

    /**
     * trip_headsign
     * @type {String}
     */
    this.headsign = '';

    /**
     * route_id
     * @type {String}
     */
    this.routeId = '';

    /**
     * service_id
     * @type {String}
     */
    this.serviceId = '';

    /**
     * direction_id
     * @type {Number}
     */
    this.directionId = 0;

    /**
     * block_id
     * @type {String}
     */
    this.blockId = '';

    /**
     * shape_id
     * @type {String}
     */
    this.shapeId = '';

    /**
     * Parent route
     * @type {Route}
     */
    this.route = null;

    /**
     * Stoptime retrieved
     * @type {Boolean}
     */
    this.stoptimeRetrieved_ = false;

  }

  Trip.prototype = new Base();

  /**
   * Current Trip object selected
   * @type {Trip}
   */
  Trip.current = null;


  /**
   * Set route for Trip object
   * @param {Route} route Route object
   */
  Trip.prototype.setRoute = function(route) {
    this.route = route;
  };

  /**
   * Reset Trip node to it's initial state and set Stop status to false
   */
  Trip.prototype.resetDisplay = function() {
    this.resetDomNode();
    this.setStopStatus(false);
    this.resetShapes();
  };

  /**
   * Reset Trip node to it's initial state
   */
  Trip.prototype.resetDomNode = function() {
    this.node.className = 'tripChoice';
  };

  /**
   * Create DOM node for display
   * @param {Boolean} notShow Not show node if true
   * @return {HTMLElement} Span node
   */
  Trip.prototype.createNode = function(notShow) {
    var node = $C('span');
    node.id = 'trip_' + this.id;
    node.className = 'tripChoice';
    node.innerHTML = this.startTime || this.id;
    GEvent.bindDom(node, 'click', this, this.onSelect);
    if (notShow) {
      node.style.display = 'none';
    }
    this.node = node;
    return this.node;
  };

  /**
   * Handler for Trip selection
   */
  Trip.prototype.onSelect = function() {
    this.triggerEvent('click', {
      trip: this
    });
    this.node.className = 'tripChoiceSelected';
    var self = this;
    map.clearOverlays();
    this.initToolBar();
    this.getStoptimes(function() {
      self.showStoptimes();
      self.getShapes(function() {
        self.showShapes();
        self.showInfoOfTripRouteAndPattern();
      });
    });
  };

  /**
   * Handler for Trip selection
   */
  Trip.prototype.initToolBar = function() {
    if (!this.toolbar) {
      this.toolbar = new InnerToolbar();
    }
    if (!Trip.toolbar || Trip.toolbar != this.toolbar) {
      if (Trip.toolbar)
        map.removeControl(Trip.toolbar);
      map.addControl(this.toolbar);
      GEvent.bindDom(this.toolbar.verifyButton, 'click', this, this.onVerify);
      Trip.toolbar = this.toolbar;
    }
  };

  /**
   * Event handler for verify button
   */
  Trip.prototype.onVerify = function() {
    var info = 'This action will clear changes you made previously, ' + 
      'Sure to continue?';
    if (!confirm(info)) {
      return;
    }
    var self = this;
    this.shape.hide();
    this.verifyMultiPoint(function() {
      // After the change of the Leg, 
      // verification on Leg are changed accordingly.
      // As to weiliu's advise, 
      // verification will always break the previous shape.
      self.shape.setColor(Leg.Color.MODIFIED);
      self.showShapes();
    }, true);
    Shape.recordModify(this.shape);
  };

  /**
   * Trip verifying function with more points in GDirections
   * @param {Function} callback Callback function for verification
   */
  Trip.prototype.verifyMultiPoint = function(callback) {
    map.closeInfoWindow();
    var maxPoint = 25;
    var idx = 0;
    var self = this;
    var shape = this.shape;
    var stopNumber = self.stoptimes.length;
    var route = self.route;
    var hintNode = (this.toolbar && this.toolbar.hint) || $('gnote');
    var points = [self.stoptimes[0].stop.getLatLng()]

    var getStopLatLng = function(idx) {
      return self.stoptimes[idx].stop.getLatLng();
    }

    /**
     * Get query string for multi points driving direction
     * @param {Number} start Starting index of the stoptimes
     */
    var getQueryString = function(start) {
      var query = 'from: ' + getPoint(getStopLatLng(start));
      for (var i = 1; (i < maxPoint) && (start + i < stopNumber);
        i++) {
        query += ' to: ' + getPoint(getStopLatLng(start + i));
        idx++;
      }
      return query;
    }

    /**
     * Verify with multi points
     * @param {GPolyline} polyline Polyline returned by Map's API
     * @param {Object} error Status returned by Map's API
     */
    var verifyMulti = function(query) {
      getDirPolyline(query, self, function(polyline, error) {
        if (!polyline) {
          hintNode.innerHTML = 'Error on index <b style="color:red">'
            + (idx + 1) + '</b>, code: ' + error.code;
          if (callback) {
            callback(false, idx);
          }
          return;
        }
        var count = polyline.getVertexCount();
        for (var i = 1; i < count; i++) {
          points.push(polyline.getVertex(i));
        }
        startDir();
      });
    }

    /**
     * Select start Stop and end Stop, Create a Leg on them,
     * and get directions of the leg
     */
    var startDir = function() {
      if (idx == stopNumber - 1) {
        shape.setPoints(points);
        hintNode.innerHTML = '<b style="color:red">Succeeded!</b>';
        if (callback) {
          callback(true, idx);
        }
        return;
      }

      var hint = 'Generating Shape [' + shape.id +
        '] in route ' + route.id;
      hint += ', from <b style="color:red"> ' + (idx + 1) + '</b>';
      var query = getQueryString(idx);
      hint += ' to ' + (idx + 1) ;
      hintNode.innerHTML = hint;

      verifyMulti(query);
    };
    startDir();
  };

  /**
   * Show related Trip, Route and Pattern information in bottombar
   * for current Trip object
   */
  Trip.prototype.showInfoOfTripRouteAndPattern = function() {
    var tripInfo = this.getInfoHtml() + '<br/>';
    var routeInfo = this.route.getInfoHtml() + '<br/>';
    var svgInfo = this.getSVGTag('/ttablegraph?height=100&trip=' + this.id,
      {height: '115', width: '100%'});
    var infoNode = $('bottombar');
    infoNode.style.display = 'block';
    infoNode.innerHTML = tripInfo + routeInfo + svgInfo;
    var height = infoNode.offsetHeight + infoNode.style.marginTop;
    var top = $('topbar');
    var content = $('content');
    height = windowHeight() - top.offsetHeight - 15 - height;
    content.style.height = height + 'px';
    if (map) {
      map.checkResize();
    }
  };

  /**
   * Get StopTime object by stop
   */
  Trip.prototype.getStopTimeByStop = function(stop) {
    for(var i = 0; i < this.stoptimes.length; i++) {
      var stoptime = this.stoptimes[i];
      if (stoptime.stop == stop) {
        return stoptime;
      }
    }
    return null;
  }

  /**
  * Return HTML to embed a SVG object in this page. src is the location of
  * the SVG and attributes is inserted directly into the object or embed
  * tag.
  * @param {String} src URL of the svg source
  * @param {Object} attributes Attributes for svg object
  * @return {String} String in HTML format to describe failure or svg object
  */
  Trip.prototype.getSVGTag = function(src, attributes) {
    var strAttr = '';
    for (var key in attributes) {
      strAttr += ' ' + key + '="' + attributes[key] + '"';
    }
    if (navigator.userAgent.toLowerCase().indexOf('msie') != -1) {
      if (isSVGControlInstalled()) {
        return '<embed pluginspage="http://www.adobe.com/svg' +
            '/viewer/install/" src="' + src + '"' +
            strAttr + '></embed>';
      } else {
        return '<p>Please install the <a href="' +
            'http://www.adobe.com/svg/viewer/install/">Adobe SVG Viewer</a>' +
            'to get SVG support in IE</p>';
      }
    } else {
      return '<object data="' + src + '" type="image/svg+xml" ' +
          strAttr + '><p>No SVG support in your browser. Try Firefox 1.5 or ' +
          'newer or install the <a href="' +
          'http://www.adobe.com/svg/viewer/install/">Adobe SVG Viewer' +
          '</a></p></object>';
    }
  };

  /**
  * Get Html info of Trip
  * @return {String} Trip info in HTML
  */
  Trip.prototype.getInfoHtml = function() {
    var tripAttrPairs = {
      id: 'trip_id',
      headsign: 'trip_headsign',
      routeId: 'route_id',
      serviceId: 'service_id',
      directionId: 'direction_id',
      blockId: 'block_id',
      shapeId: 'shape_id'
    };

    var str = '<b>trips.txt</b>:';
    for (var key in tripAttrPairs) {
      str += '&nbsp;&nbsp;<b>' + tripAttrPairs[key] + '</b>: ' + this[key];
    }
    return str;
  };

  /**
   * Get Shapes info
   * @param {Function} callback A function invoked on the end of the action
   */
  Trip.prototype.getShapes = function(callback) {
    if (shapesFetched[this.shapeId]) {
      this.shape = shapesFetched[this.shapeId];
      if (callback) callback();
      return;
    }
    if (this.inAsync) return;
    this.fetcher.getShapes(this.id, this, function(data) {
      this.parseShapes(data);
      if (callback) callback();
      this.inAsync = false;
    });
  };

  /**
   * Show Shapes
   */
  Trip.prototype.showShapes = function() {
    this.shape.show();
  };

  /**
   * Hide Shapes
   */
  Trip.prototype.resetShapes = function() {
    this.shape.hide();
  };

  /**
   * Parse Shapes info into Trip members
   * @param {Array} data JSON evaled data
   */
  Trip.prototype.parseShapes = function(data) {
    var shape = new Shape();
    shape.id = this.shapeId;
    shape.data = data;
    shape.stops = this.stops;
    shape.metaPoints = data;
    shape.bindStops();
    this.shape = shape;
    shapesFetched[this.shapeId] = shape;
  };

  /**
   * Get Stoptimes info
   * @param {Function} callback A function invoked on the end of the action
   */
  Trip.prototype.getStoptimes = function(callback) {
    if (this.stoptimeRetrieved_) {
      if (callback) callback();
      return;
    }
    if (this.inAsync) return;
    this.fetcher.getStoptimes(this.id, this, function(data) {
      this.parseStoptimes_(data);
      if (callback) callback();
      this.stoptimeRetrieved_ = true;
      this.inAsync = false;
    });
  };

  /**
   * Parse Stoptimes info into Trip members
   * @param {Array} data JSON evaled data
   * @private
   */
  Trip.prototype.parseStoptimes_ = function(data) {
    if (!data || !data.length) return;
    this.stoptimes = [];
    var stopsOfShapeNotInitialized = false;
    if (!stopsOfShape[this.shapeId]) {
      stopsOfShape[this.shapeId] = [];
      stopsOfShapeNotInitialized = true;
    }
    this.stops = stopsOfShape[this.shapeId];

    for (var i = 0; i < data.length; i++) {
      var stoptime = new StopTime();

      stoptime.arrivalTime = data[i].arrivalTime;
      stoptime.departureTime = data[i].departureTime;
      stoptime.trip = this;
      stoptime.sequence = data[i].sequence - 0;
      if (stopsOfShapeNotInitialized) {
        var stop = Stop.confirmAndGetStop(data[i].stop);
        this.stops.push(stop);
        stoptime.stop = stop;
      } else {
        stoptime.stop = this.stops[i];
      }
      stoptime.stop.createLabeledMarker(stoptime.arrivalTime);
      this.stoptimes.push(stoptime);
    }
    this.initAddStopFromPattern_();
  };

  /**
   * Determine if the stop is to be deleted
   * @param {Stop} stop Stop to be determined
   * @return {Boolean} Result of determine
   */
  Trip.prototype.isDeleteStop = function(stop) {
    if (actionFind('pattern', 'deletestoptime', 'stop', stop)) {
      return true;
    }
    return false;
  }

  /**
   * Parse added Stoptimes info into Trip members
   * @private
   */
  Trip.prototype.initAddStopFromPattern_ = function() {
    var route = this.route;
    if (!route.addedPatternStops) {
      return;
    }
    if (!route.addedPatternStops[this.patternId]) {
      return;
    }

    var patterns = route.addedPatternStops[this.patternId];
    for (var i = 0; i < patterns.length;  i++) {
      var pattern = patterns[i];
      var idx = pattern[1];
      var stop = pattern[0];
      var sequence = pattern[2];
      var stoptime = this.createStoptime(stop, sequence);
      this.stoptimes.splice(idx, 0, stoptime);
    }
  }

  /**
   * Handler for stop copied
   * @param {StopTime} stoptime Stoptime for saving
   * @param {Function} cb Callback function
   */
  Trip.prototype.onStopCopied = function(stoptime, cb) {
    var param = {};
    param.act = 'updatestop';
    param.rid = this.rid;
    param.seq = stoptime.sequence;
    param.sid = stoptime.stop.id;
    param.pid = this.patternId;
    this.fetcher.updatePattern(param, this, function(retCode) {
      this.onPatternStopUpdated(retCode, stoptime.stop, stoptime.sequence)
      cb();
    });
  };

  /**
   * Handler for stop added
   * @param {StopTime} stoptime Stoptime for saving
   * @param {Function} cb Callback function
   * @param {Number} sequence Sequence at inserting time
   */
  Trip.prototype.onStopAdded = function(stoptime, cb, sequence) {
    var param = {};
    param.act = 'updatestoptime';
    param.rid = this.rid;
    param.tid = this.id;
    param.seq = sequence;
    param.sid = stoptime.stop.id;
    param.atime = stoptime.arrivalTime;
    param.dtime = stoptime.departureTime;
    param.headsign = stoptime.stop.headsign;
    param.pid = this.patternId;
    param.isend = stoptime.isEnd;
    this.fetcher.updatePattern(param, this, function(retCode) {
      this.onPatternStoptimeUpdated(retCode, stoptime.stop, stoptime.sequence)
      cb();
    });
  };

  /**
   * Update stoptime stop info by sequence
   * @param {Stop} stop Stop to be assigned to stoptime
   * @param {Number} sequence Stoptime sequence
   */
  Trip.prototype.updateStopBySequence = function(stop, sequence) {
    if (!this.stoptimes) return;
    for (var i = 0, stoptime; i < this.stoptimes.length; i++) {
      stoptime = this.stoptimes[i];
      if (stoptime.sequence >= sequence) {
        if (stoptime.stop == stop) {
          continue;
        } else {
          stoptime.sequence += 1;
        }
      }
    }
  };

  /**
   * Update stoptime by sequence to all trips of the patttern
   * @param {Number} retCode Error code from server
   * @param {Stop} instop Stop to be assigned to stoptime
   * @param {Number} insequence Stoptime sequence
   */
  Trip.prototype.onPatternStoptimeUpdated = function(retCode, instop,
    insequence) {
    if (retCode == 1) {
      var route = this.route;
      var patterns = route.addedPatternStops[this.patternId];
      for (var i = 0; i < patterns.length; i++) {
        var pattern = patterns[i];
        var stop = pattern[0];
        var sequence = pattern[2];
        if ((insequence == sequence) && (instop == stop)) {
          patterns.splice(i, 1);
        }
      }
    }
    this.modified = true;
    this.inAsync = false;
  };

  /**
   * Update stoptime stop info by sequence to all trips of the patttern
   * @param {Number} retCode Error code from server
   * @param {Stop} stop Stop to be assigned to stoptime
   * @param {Number} sequence Stoptime sequence
   */
  Trip.prototype.onPatternStopUpdated = function(retCode, stop, sequence) {
    if (retCode == 1) {
      var route = this.route;
      var trips = route.patterns[this.patternId];
      for (var i = 0, trip; trip = trips[i]; i++) {
        trip.updateStopBySequence(stop, sequence);
      }
    }
    this.modified = true;
  };

  /**
   * Show stoptime
   */
  Trip.prototype.showStoptimes = function() {
    var bounds = new GLatLngBounds();
    for (var i = 0; i < this.stoptimes.length; i++) {
      var stop = this.stoptimes[i].stop;
      stop.setStatus(true);
      if (!this.isDeleteStop(stop)) {
        stop.show();
      }
      stop.setText(this.stoptimes[i].arrivalTime);
      bounds.extend(stop.getLatLng());
    }
    map.setCenter(bounds.getCenter(), map.getBoundsZoomLevel(bounds));
  };

  /**
   * Set stop to current or not
   * @param {Boolean} isTripSelected The trip is selected or not
   */
  Trip.prototype.setStopStatus = function(isTripSelected) {
    for (var i = 0, stop; stop = this.stops[i]; i++) {
      stop.setStatus(isTripSelected);
    }
  };

  /**
   * Set stops to be editable or not
   * @param {Boolean} editable Editable or not
   */
  Trip.prototype.enableStopEdit = function(editable) {
    for (var i = 0, stop; stop = this.stops[i]; i++) {
      stop.setEditable(editable);
    }
  };

  /**
   * Trip.mainPolyline is the first polyline we can find(Must can be found)
   */
  Trip.mainPolyline = null;

  /**
   * Trip.secondPolyline is the next polyline we can find(Optional)
   */
  Trip.secondPolyline = null;

  /**
   * Trip.nearestLeg is to indicate the leg shortest from the current mouse
   * position by direction.
   */
  Trip.nearestLeg = null;

  /**
   * Clear the temporary objects of adding stops
   */
  Trip.prototype.clearAddStopPolyline = function() {
    if (Trip.mainPolyline) {
      map.removeOverlay(Trip.mainPolyline);
      Trip.mainPolyline = null;
    }
    if (Trip.secondPolyline) {
      map.removeOverlay(Trip.secondPolyline);
      Trip.secondPolyline = null;
    }
    Trip.clearNearestLeg();
  };

  Trip.clearNearestLeg = function() {
    if (Trip.nearestLeg) {
      Trip.nearestLeg.hide();
      Trip.nearestLeg.show();
      Trip.nearestLeg = null;
    }
  };
  /**
   * Handler for main polyline found
   * @param {GPolyline} polyline An object of GPolyline
   */
  Trip.prototype.onMainPolylineFound = function(polyline) {
    if (polyline) {
      Trip.mainPolyline = polyline;
      polyline.setStrokeStyle({
        color: Leg.Color.FROMPOLY
      });
      map.addOverlay(polyline);
    }
  };

  /**
   * Handler for second polyline found
   * @param {GPolyline} polyline An object of GPolyline
   */
  Trip.prototype.onSecondPolylineFound = function(polyline) {
    if (polyline) {
      Trip.secondPolyline = polyline;
      polyline.setStrokeStyle({
        color: Leg.Color.TOPOLY
      });
      map.addOverlay(polyline);
    }
  };

  /**
   * Create a new object of StopTime with given stop and sequence
   * @param {Stop} stop An object of Stop
   * @param {Number} sequence Sequence of the StopTime object
   * @return {StopTime} new StopTime
   */
  Trip.prototype.createStoptime = function(stop, sequence) {
    var stoptime = new StopTime();
    stoptime.arrivalTime = '';
    stoptime.departureTime = '';
    stoptime.trip = this;
    stoptime.sequence = sequence;
    stoptime.stop = stop;

    stoptime.stop.createLabeledMarker(stoptime.arrivalTime);
    stoptime.modified = true;
    this.modified = true;
    return stoptime;
  }

  /**
   * Create a new object of Stop with given latlng
   * @param {GLatLng} latlng Position of the stop
   * @return {Stop} new Stop
   */
  Trip.prototype.createTripStop = function(latlng) {
    var newStop = new Stop();
    newStop.name = 'addstop';
    newStop.lat = latlng.lat();
    newStop.lng = latlng.lng();
    newStop.desc = 'inserted';
    newStop.locationType = 0;
    var id = Stop.generateAddId();
    newStop.id = id;
    newStop.latlng = new GLatLng(newStop.lat, newStop.lng);
    newStop.createMarker();
    newStop.isTripSelected = true;
    newStop.headsign = '';
    newStop.modified = true;
    return newStop;
  };

  /**
   * Insert  a new object of StopTime by given stop and sequence
   * @param {Stop} stop An object of Stop
   * @param {Number} idx Index of the StopTime object to be inserted
   */
  Trip.prototype.insertStoptime = function(stop, idx) {
    var route = this.route;
    var trips = route.patterns[this.patternId];
    var sequence = null;

    if (idx == this.stops.length) {
      sequence = this.stoptimes[idx - 1].sequence + 1;
    } else {
      sequence = this.stoptimes[idx].sequence;
    }
    route.addPatternStopEnvInfo(this.patternId, stop, idx, sequence);
    for (var i = 0, trip; trip = trips[i]; i++) {
      if (!trip.stoptimes) {
        continue;
      }
      var len = trip.stoptimes.length;
      if (len) {
        var stoptime = trip.createStoptime(stop, sequence);
        stoptime.isEnd = 0;

        // Arrival/departure time is required on generating svg graphics
        // and the starting point of the ending point must have this value
        // or the svg generator will fail to generate proper graph of
        // the pattern.
        // So as a temporary solution I copy the former points on the ends
        // as the new stop's default value.

        if (idx == 0) {
          stoptime.arrivalTime = this.stoptimes[idx].arrivalTime;
          stoptime.departureTime = this.stoptimes[idx].departureTime;
        }
        if (idx == this.stops.length) {
          stoptime.arrivalTime = this.stoptimes[idx - 1].arrivalTime;
          stoptime.departureTime = this.stoptimes[idx - 1].departureTime;
          stoptime.isEnd = 1;
        }
        trip.stoptimes.splice(idx, 0, stoptime);
        if (trip == this) {
          actionList.push({type:'stop', act: 'add',
            obj: {stop: stop, stoptime: stoptime, sequence: sequence}
          });
          Shape.recordModify(this.shape);
        }
        for (var j = idx + 1; j <= len; j++) {
          stoptime = trip.stoptimes[j];
          stoptime.sequence += 1;
        }
      }
    }
  };


 /**
   * Create new Leg with given polyline and display it
   * @param {Stop} from From stop
   * @param {Stop} to To stop
   * @param {GPolyline} polyline Polyline for newly created Leg
   */
  Trip.prototype.createLegWithPolylineAndShow = function(from, to, polyline) {
    this.shape.createLeg(from, to);
    from.toLeg[this.shape.id].setPolyline(polyline);
    from.toLeg[this.shape.id].show();
  };

 /**
   * Insert a new object of Stop to by given stop and sequence
   * @param {Stop} stop An object of Stop
   * @param {Number} idx Index of the StopTime object to be inserted
   */
  Trip.prototype.insertTripStop = function(stop, idx) {
    stop.show();
    if (this.stops) {
      this.stops.splice(idx, 0, stop);
    }
  };

 /**
   * Add Stop at the start stop of the shape
   * @param {GLatLng} pos Position for the new stop
   * @param {GPolyline} polyline Polyline for newly created Leg
   */
  Trip.prototype.addHeadStop = function(pos, polyline) {
    var idx = 0;

    var newStop = this.createTripStop(pos);
    this.createLegWithPolylineAndShow(newStop, this.stops[idx], polyline);
    this.insertStoptime(newStop, idx);
    this.insertTripStop(newStop, idx);
    this.inAsync = false;
  };

 /**
   * Add Stop at the end stop of the shape
   * @param {GLatLng} pos Position for the new stop
   * @param {GPolyline} polyline Polyline for newly created Leg
   */
  Trip.prototype.addRearStop = function(pos, polyline) {
    var idx = this.stops.length;
    var newStop = this.createTripStop(pos);
    this.createLegWithPolylineAndShow(this.stops[idx - 1], newStop, polyline);
    this.insertStoptime(newStop, idx);
    this.insertTripStop(newStop, idx);
    this.inAsync = false;
  };

 /**
   * Add Stop in the middle of the shape
   * @param {GLatLng} from Position of from stop
   * @param {GLatLng} pos Position for the new stop
   * @param {GLatLng} to Position of to stop
   * @param {GPolyline} polyline Polyline of main polyline
   * @param {GPolyline} polyline1 Polyline of second polyline
   * @param {Number} idx Index of the Stop object to be inserted
   */
  Trip.prototype.addMidStop = function(from, pos, to, polyline, polyline1, idx)
  {
    idx = idx + 1;
    var newStop = this.createTripStop(pos);
    this.createLegWithPolylineAndShow(this.stops[idx - 1], newStop, polyline);
    this.createLegWithPolylineAndShow(newStop, this.stops[idx], polyline1);
    this.insertStoptime(newStop, idx);
    this.insertTripStop(newStop, idx);
    this.inAsync = false;
  };

 /**
   * Delete stop from pattern
   * @param {Stop} stop Stop to be deleted
   * @param {Boolean} isSetDelete Set stoptime delete tag or not
   */
  Trip.prototype.onDeleteStop = function(stop, isSetDelete) {
    var shape = this.shape;
    for (var i = 0; i < this.stops.length; i++) {
      if (this.stops[i] == stop) {
        var fromLeg = null;
        if (stop.fromLeg) fromLeg = stop.fromLeg[shape.id];
        var toLeg = null;
        if (stop.toLeg) toLeg = stop.toLeg[shape.id]
        if (i == 0) {
          this.stops[i + 1].fromLeg[shape.id] = null;
          toLeg.hide();
          stop.toLeg[shape.id] = null;
        } else if (i == this.stops.length - 1) {
          this.stops[i - 1].toLeg[shape.id] = null;
          fromLeg.hide();
          stop.fromLeg[shape.id] = null;
        } else {
          fromLeg.hide();
          fromLeg.clearListeners();
          toLeg.hide();
          toLeg.clearListeners();
          stop.fromLeg[shape.id] = null;
          stop.toLeg[shape.id] = null;
          this.stops[i - 1].toLeg[shape.id] = null;
          this.stops[i + 1].fromLeg[shape.id] = null;
          var leg = shape.createLeg(this.stops[i - 1], this.stops[i + 1]);
          this.stops[i - 1].toLeg[shape.id] = leg;
          this.stops[i + 1].fromLeg[shape.id] = leg;
          leg.getGDirections();
        }
        stop.hide();
        this.stops.splice(i, 1);
        actionList.push({type: 'pattern', act: 'deletestoptime',
          obj: {stoptime: this.stoptimes[i], stop: stop}
        });
        Shape.recordModify(shape);
        return;
      }
    }
  };

  /**
   * Get sequence of the stop in stoptimes
   * @param {Stop} stop Stop to be found
   * @return {Number} Sequence of the stoptime having the stop
   */
  Trip.prototype.getStopSequence = function(stop) {
    if (!this.stoptimes) return -1;
    for (var i = 0, len = this.stoptimes.length; i < len; i++) {
      if (this.stoptimes[i].stop == stop) {
        return this.stoptimes[i].sequence;
      }
    }
    return -1;
  }

  /**
   * Get index of the stoptime in stoptimes
   * @param {StopTime} stoptime StopTime to be found
   * @return {Number} Index of the stoptime in stoptimes
   */
  Trip.prototype.getStoptimeIndex = function(stoptime) {
    for (var i = 0, len = this.stoptimes.length; i < len; i++) {
      if (this.stoptimes[i] == stoptime) {
        return i;
      }
    }
    return -1;
  }

  /**
   * Delete stoptime from pattern
   * @param {StopTime} stoptime StopTime to be deleted
   */
  Trip.prototype.deleteStoptime = function(stoptime) {
    for (var i = 0; i < this.stoptimes.length; i++) {
      if (this.stoptimes[i] == stoptime) {
        this.stoptimes.splice(i, 1);
        return;
      }
    }
  }

 /**
   * Handler for adding Stop objects to the shape
   * @param {GLatLng} pos Position for the new stop
   */
  Trip.prototype.onAddStop = function(pos) {
    this.addStopPosUpdate(pos, true);
  };

 /**
   * Recalculate distance travelled
   * @param {GLatLng} pos Position for the new stop
   */
  Trip.prototype.recalculateDistance = function(pos) {
    this.addStopPosUpdate(pos, true);
  };

 /**
   * Handler for adding Stop objects to the shape
   * @param {GLatLng} pos Position for the new stop
   * @param {Boolean} add Adding stops or not
   */
  Trip.prototype.addStopPosUpdate = function(pos, add) {
    if (this.inAsync) {
      return;
    }
    this.inAsync = true;
    var maxDistance = 300;
    this.clearAddStopPolyline();
    var legInfo = this.shape.findNearestLeg(pos);
    var idx = legInfo.idx;
    if (idx == -1) {
      return;
    }
    var dist = legInfo.distance;
    if (dist > maxDistance) {
      this.inAsync = false;
      return;  //keep it in a limited area
    }

    var leg = legInfo.leg;
    var stop = legInfo.stop;
    if (!leg) {
    //leg is null when the cursor is near to the edge of the shape/trip
      var cb = null;
      var from = null;
      var to = null;
      if (idx == 0) {
        leg = stop.toLeg[this.shape.id];
        from = pos;
        to = stop.getLatLng();
        cb = this.addHeadStop;
      } else {
        leg = stop.fromLeg[this.shape.id];
        from = stop.getLatLng();
        to = pos;
        cb = this.addRearStop;
      }
      Trip.nearestLeg = leg;
      leg.setColor(Leg.Color.SELECTED);
      var query = 'from: ' + getPoint(from) + ' to: ' + getPoint(to);
      getDirPolyline(query, this, function(poly) {
        this.onMainPolylineFound(poly);
        if (add) {
          cb.call(this, pos, poly);
        }
        this.inAsync = false;
      });
      return;
    }
    Trip.nearestLeg = leg;
    leg.setColor(Leg.Color.SELECTED);

    var addMidStop = null;
    if (add) {
      addMidStop = this.addMidStop;
    }

    this.showAddStopInMid(leg.fromStop.getLatLng(),
        pos, leg.toStop.getLatLng(), idx, addMidStop);
  }

 /**
   * Add Stop in the middle of the shape
   * @param {GLatLng} from Position of from stop
   * @param {GLatLng} pos Position for the new stop
   * @param {GLatLng} to Position of to stop
   * @param {Number} idx Index of the Stop object to be inserted
   * @param {Function} callback Callback function of the adding stops
   */
  Trip.prototype.showAddStopInMid = function(from, pos, to, idx, callback) {
    this.inAsync = true;
    var query = 'from: ' + getPoint(from) + ' to: ' + getPoint(pos);
    getDirPolyline(query, this, function(poly) {
      this.onMainPolylineFound(poly);
      var query = 'from: ' + getPoint(pos) + ' to: ' + getPoint(to);
      getDirPolyline(query, this, function(poly1) {
        this.onSecondPolylineFound(poly1);
        if (callback) {
          callback.call(this, from, pos, to, poly, poly1, idx);
          return;
        }
        this.inAsync = false;
      });
    });
  }

  /**
   * Check relationships for current position
   * @param {GLatLng} pos Position of the mouse pos
   */
  Trip.prototype.checkAddStopStatus = function(pos) {
    this.addStopPosUpdate(pos, false);
  }

  /**
   * Shape is a class representing a GTFS shape.
   * @constructor
   */
  function Shape() {
    Base.call(this);

    /**
     * shape_id
     * @type {String}
     */
    this.id = '';

    /**
     * Array of points having latitude and longitude
     * @type {Array}
     */
    this.metaPoints = [];
  }

  Shape.prototype = new Base();

  /**
   * Handler for all shape actions
   * @param {Object} act Infomation of an action
   * @param {Function} cb Callback function
   */
  Shape.handleAct = function(act, cb) {
    switch(act.act) {
      case 'modify':
        Shape.handleModify(act, cb);
        break;
    }
  }

  /**
   * Handler for shape modifications
   * @param {Object} act Infomation of an action
   * @param {Function} cb Callback function
   */
  Shape.handleModify = function(act, cb) {
    var shape = act.obj.shape;
    shape.save(cb);
  }

  /**
   * Record shape modified
   * @param {Shape} shape Shape to be recorded
   */
  Shape.recordModify = function(shape) {
    if (!actionFind('shape', 'modify', 'shape', shape)) {
      actionList.push({type: 'shape', act: 'modify',
        obj: {shape: shape}
      });
    }
  }

  /**
   * If the distance from 'from' to 'to' is the nearer than 'dist'
   *
   * @param {GLatLng} from From node
   * @param {GLatLng} to To node
   * @param {Number} dist Minimum distance from current legs
   * @return {Boolean}
   */
  Shape.prototype.isNearer = function(from, to, dist) {
    var newDist = to.distanceFrom(from);
    if (dist > newDist) {
      return true;
    }
    return false;
  }

  /**
   * Find nearest leg to the position
   * @param {GLatLng} pos Position of current node
   * @return {Object}
   */
  Shape.prototype.findNearestLeg = function(pos) {
    var curLeg = null;
    var idx = -1;
    var minDistance = INFINITY;
    var stop = null;
    for (var i = 0; i < this.stops.length - 1; i++) {
      var tmpStop = this.stops[i];
      var leg = tmpStop.toLeg[this.id];
      leg.setColor(Leg.Color.DEFAULT);
      var dist = leg.nearestDistance(pos);
      if (dist < minDistance) {
        curLeg = leg;
        idx = i;
        minDistance = dist;
        stop = tmpStop;
      }
    }
    var head = this.stops[0].getLatLng();
    var rear = this.stops[this.stops.length - 1].getLatLng();
    dist = head.distanceFrom(pos);

    if (dist <= minDistance) {
        curLeg = null;
        idx = 0;
        minDistance = dist;
        stop = this.stops[0];

    }
    dist = pos.distanceFrom(rear);
    if (dist <= minDistance) {
        curLeg = null;
        idx = this.stops.length - 1;
        minDistance = dist;
        stop = this.stops[this.stops.length - 1];
    }

    return {
      leg: curLeg,
      idx: idx,
      distance: minDistance,
      stop: stop
    };
  };

  /**
   * Get the index of the points nearest to latlng
   * @param {Array} points Shape points to compared with
   * @param {Number} start Index to start with
   * @param {GLatLng} latlng Point to find nearest points
   * @return {Number} Index of the point found
   */
  Shape.prototype.getNearestPoint = function(points, start, latlng) {
    if (!points.length) return - 1;
    var minDistance = INFINITY;
    var found = points.length - 1;
    var cur = - 1;
    for (var i = start; i <= found; i++) {
      var latlngOfPoint = new GLatLng(points[i].lat, points[i].lng);
      var dis = latlng.distanceFrom(latlngOfPoint);
      if ((dis < minDistance)) {
        minDistance = dis;
        cur = i;
      }
    }
    if (cur > 0) return cur;
    return found;
  };

  /**
   * Create Verifying Leg with init from stop and to stop
   * @param {Stop} fromStop From Stop of the Leg
   * @param {Stop} toStop To Stop of the Leg
   * @return {Leg} Leg object
   */
  Shape.prototype.createVerifyLeg = function(fromStop, toStop) {
    if (!toStop.fromVerifyLeg) {
      toStop.fromVerifyLeg = {};
    }
    if (!fromStop.toVerifyLeg) {
      fromStop.toVerifyLeg = {};
    }
    var leg = fromStop.toVerifyLeg[this.id];
    if (!leg) {
      leg = new Leg();
    }
    toStop.fromVerifyLeg[this.id] = leg;
    fromStop.toVerifyLeg[this.id] = leg;
    leg.setStops(fromStop, toStop);
    this.listenToLeg(leg);
    return leg;
  };

  /**
   * Create Leg with init from stop and to stop
   * @param {Stop} fromStop From Stop of the Leg
   * @param {Stop} toStop To Stop of the Leg
   * @param {Array} points Array of points
   * @param {String} color String of CSS color
   * @return {Leg} Leg object
   */
  Shape.prototype.createLeg = function(fromStop, toStop, points, color) {
    if (!toStop.fromLeg) {
      toStop.fromLeg = {};
    } else {
      if (toStop.fromLeg[this.id]) {
        toStop.fromLeg[this.id].hide();
      }
    }
    if (!fromStop.toLeg) {
      fromStop.toLeg = {};
    } else {
      if (fromStop.toLeg[this.id]) {
        fromStop.toLeg[this.id].hide();
      }
    }
    var leg = fromStop.toLeg[this.id];
    if (!leg) {
      leg = new Leg();
    }
    toStop.fromLeg[this.id] = leg;
    fromStop.toLeg[this.id] = leg;
    leg.setStops(fromStop, toStop);
    this.listenToLeg(leg);
    leg.setPoints(points, color);
    leg.addListener('reload', this, this.onLegReload);
    return leg;
  };

  /**
   * Handler for Leg dragend event
   */
  Shape.prototype.onDragEnd = function() {
    this.modified = true;
    Shape.recordModify(this);
  }

  /**
   * Listen to the events of legs
   * @param {Leg} leg An instance of Leg
   */
  Shape.prototype.listenToLeg = function(leg) {
    leg.addListener('dragend', this, this.onDragEnd);
  }

  /**
   * Handler on leg reload
   */
  Shape.prototype.onLegReload = function() {
    this.modified = true;
    Shape.recordModify(this);
  };

  /**
   * Set points of the shape and rebuild the shape infomation
   * @param {Array} points Array of position latlngs
   */
  Shape.prototype.setPoints = function(points) {
    var metaPoints = [];
    for (var i = 0 ; i < points.length; i++) {
      metaPoints.push({lat: points[i].lat(), lng: points[i].lng()});
    }
    this.metaPoints = metaPoints;
    this.bindStops();
  }

  /**
   * Bind Stops with Legs
   */
  Shape.prototype.bindStops = function() {
    if (!this.metaPoints.length) {
      for (var i = 1; i < this.stops.length; i++) {
        this.createLeg(this.stops[i - 1], this.stops[i]);
      }
      return;
    }

    var start = 0;
    var end = - 1;
    for (i = 1; i < this.stops.length; i++) {
      end = this.getNearestPoint(this.metaPoints, start,
        this.stops[i].getLatLng());
      var points = [];
      for (var j = start; j <= end; j++) {
        points.push(this.metaPoints[j]);
      }
      this.createLeg(this.stops[i - 1], this.stops[i], points);
      start = end;
    }
  };

  /**
   * Show Legs of Shape
   */
  Shape.prototype.show = function(trip) {
    for (var i = 0; i < this.stops.length - 1; i++) {
      if (!trip || !trip.isDeleteStop(this.stops[i])) {
        if (this.stops[i].toLeg[this.id]) {
          this.stops[i].toLeg[this.id].show();
        }
      }
    }
  };


  /**
   * Set color of Legs
   */
  Shape.prototype.setColor = function(color) {
    for (var i = 0; i < this.stops.length - 1; i++) {
      if (this.stops[i].toLeg[this.id]) {
        this.stops[i].toLeg[this.id].setColor(color);
      }
    }
  }


  /**
   * Replace old stop with new stop
   */
  Shape.prototype.replaceStop = function(from, to) {
    for (var i = 0; i < this.stops.length - 1; i++) {
      if (this.stops[i] == from) {
        this.stops[i] = to;
      }
    }
  };

  /**
   * Hide Legs of Shape
   */
  Shape.prototype.hide = function() {
    for (var i = 0; i < this.stops.length - 1; i++) {
      if (this.stops[i].toLeg[this.id]) {
        this.stops[i].toLeg[this.id].hide();
      }
    }
  };

  /**
   * Set Leg status to saved
   */
  Shape.prototype.setLegAsSaved = function() {
    for (var i = 0; i < this.stops.length - 1; i++) {
      var leg = this.stops[i].toLeg[this.id];
      if (leg) {
        leg.setColor(Leg.Color.DEFAULT);
        leg.modified = false;
      }
    }
  };

  /*
   * Save the shape to the server side
   */
  Shape.prototype.save = function(callback) {
    if (this.inAsync) {
      return;
    }
    var postBody = this.serialize();

    this.fetcher.saveShape(postBody, this, function(data) {
      this.onSaved(data);
      this.setLegAsSaved();
      if (callback) {
        callback();
      }
    });
  };

  /*
   * Handler on saving end
   */
  Shape.prototype.onSaved = function() {
    this.modified = false;
  };

  /*
   * Unit shape waypoint string for saving
   * @param {GLatLng} point A point in GLatLng format
   * @param {Number} distance The distance from the point to the start point
   * @return {String} Formatted string of point
   */
  Shape.prototype.pointToString = function(point, distance) {
    return  + point.lat().toFixed(9) + ',' + point.lng().toFixed(9)
      + ',' + distance.toFixed(9);
  };

  /**
   * Generate post body for the Shape object
   * @return {String} Post body
   */
  Shape.prototype.serialize = function() {
    var from = this.stops[0].getLatLng();
    var to = from;
    var distance = from.distanceFrom(to) / 1000.0;
    var shapePostArray = [];
    shapePostArray.push(this.pointToString(from, distance));
    for (var i = 0; i < this.stops.length - 1; i++) {
      var leg = this.stops[i].toLeg[this.id];
      if (leg) {
        var points = leg.getPoints();
        for (var j = 1; j < points.length; j++) {
          from = points[j];
          distance += from.distanceFrom(to) / 1000.0;
          shapePostArray.push(this.pointToString(from, distance));
          to = from;
        }
      }
    }
    return 'shapeId=' + this.id + '&points=' + shapePostArray.join('&points=');
  };

  /**
   * Leg is a class representing the shape of two stops.
   * @constructor
   */
  function Leg() {
    Base.call(this);
    /**
     * Array of points having latitude and longitude
     * @type {Array}
     */
    this.points = [];

    /**
     * RGB color string
     * @type {String}
     */
    this.color = '';

    /**
     * Front Stop of the leg
     * @type {StopTime}
     */
    this.fromStop = null;

    /**
     * End Stop of the leg
     * @type {StopTime}
     */
    this.toStop = null;

    /**
     * Object of GPolyline of the leg
     * @type {StopTime}
     */
    this.polyline = null;
  }

  Leg.prototype = new Base();

  /**
   * Enum Color for legs
   * @enum {String}
   */
  Leg.Color = {
    DEFAULT: '#F00',
    MODIFIED: '#0A0',
    VERIFIED: '#00F',
    FROMPOLY: '#00F',
    TOPOLY: '#0F0',
    SELECTED: '#000'
  };

  /**
   * Draggable marker
   * @type {GMarker}
   */
  Leg.dragMarker = null;

  /**
   * Check to make sure that the firstSub is initialized
   */
  Leg.prototype.checkFirstSub = function() {
    if (!this.firstSub) {
      // The structure of the SubLegs and Anchors in Leg
      // is like the following chart:
      // startAnchor -> firstSub -> nextAnchor -> nextSub ... -> endAnchor
      // where startAnchor, endAnchor and firstSub are members of the Leg
      // while nextAnchors and nextSubs are temporary objecs with no direct
      // handlers in Leg.
      this.firstSub = new SubLeg();
      this.firstSub.fromAnchor = this.startAnchor;
      this.firstSub.toAnchor = this.endAnchor;
      this.startAnchor.setLegs(null, this.firstSub);
      this.endAnchor.setLegs(this.firstSub, null);
    }
  }

  /**
   * Set Leg with init from stop and to stop
   * @param {Stop} from From Stop of the Leg
   * @param {Stop} to To Stop of the Leg
   */
  Leg.prototype.setStops = function(from, to) {
    this.fromStop = from;
    this.toStop = to;
    this.fromListener = from.addListener('dragend', this,
      this.onFromStopDragEnd);
    this.toListener = to.addListener('dragend', this, this.onToStopDragEnd);
    this.startAnchor =  new Anchor();
    this.startAnchor.setPos(this.fromStop.getLatLng());
    this.endAnchor =  new Anchor();
    this.endAnchor.setPos(this.toStop.getLatLng());
    this.checkFirstSub();
    this.firstSub.setPoints();
  };

  /**
   * Clear listeners from stops
   */
  Leg.prototype.clearListeners = function() {
    this.fromStop.removeListener(this.fromListener);
    this.toStop.removeListener(this.toListener);
  };

  /**
   * Handler for fromStop dragging
   * @param {GLatLng} pos Position of the dragging point
   */
  Leg.prototype.onFromStopDragEnd = function(pos) {
    this.startAnchor.setPos(pos);
    this.firstSub.getGDirections();
  };

  /**
   * Handler for toStop dragging
   * @param {GLatLng} pos Position of the dragging point
   */
  Leg.prototype.onToStopDragEnd = function(pos) {
    this.endAnchor.setPos(pos);
    this.endAnchor.fromLeg.getGDirections();
  }

  /**
   * Create polyline from points and color
   * @param {Array} points points to be converted to GPolyline
   * @param {String} color RGB string of color
   */
  Leg.prototype.setPoints = function(points, color) {
    var latlngs = [];
    if (!points || !points.length) {
      latlngs = [this.fromStop.getLatLng(), this.toStop.getLatLng()];
    } else {
      for (var i = 0; i < points.length; i++) {
        latlngs.push(new GLatLng(points[i].lat, points[i].lng));
      }
    }
    color = color || Leg.Color.DEFAULT;
    this.polyline = new GPolyline(latlngs, color, 4);
    this.setPolyline(this.polyline);
  };

  /**
   * Set polyline to the Leg with color
   * @param {GPolyline} polyline polyline to be set to Leg
   * @param {String} color RGB string of color
   */
  Leg.prototype.setPolyline = function(polyline, color) {
    this.checkFirstSub();
    this.rebuild();
    this.firstSub.setPolyline(polyline, color);
  }

  /**
   * Rebuild relationships between sub, startAnchor, endAnchor
   */
  Leg.prototype.rebuild = function() {
    this.firstSub.fromAnchor = this.startAnchor;
    this.firstSub.toAnchor = this.endAnchor;
    this.startAnchor.setLegs(null, this.firstSub);
    this.endAnchor.setLegs(this.firstSub, null);
  }

  /**
   * Handler for ReGDirections
   */
  Leg.prototype.onGetGDirections = function() {
    map.closeInfoWindow();
    this.getGDirections();
  };

  /**
   * Get the directions of this Leg, pass polyline to callback
   * @param {Object} self This pointer of callback
   * @param {Function} callback Callback on getDriPolyline finished
   */
  Leg.prototype.verify = function(self, callback) {
    var start = this.fromStop.getLatLng();
    var end = this.toStop.getLatLng();
    var query = 'from: ' + getPoint(start) + ' to: ' + getPoint(end);
    var leg = this;
    getDirPolyline(query, self, callback);
  };

  /**
   * Get the directions of this Leg, with UI changes
   */
  Leg.prototype.getGDirections = function() {
    this.verify(this, this.onDirPolylineLoaded);
  };

  /**
   * Clear Anchors from Leg
   */
  Leg.prototype.clearAnchors = function() {
    var toAnchor = this.startAnchor.toLeg.toAnchor;
    while (toAnchor != this.endAnchor) {
      toAnchor.hide();
      if (toAnchor.toLeg) {
        toAnchor.toLeg.hide();
        toAnchor = toAnchor.toLeg.toAnchor;
      } else {
        break;
      }
    }
    this.rebuild();
  }

  /**
   * Remove all intermedia anchors and SubLegs
   */
  Leg.prototype.unify = function() {
    var points = this.getPoints();
    this.clearAnchors();
    this.firstSub.hide();
    this.firstSub.setPolyline(new GPolyline(points, Leg.Color.DEFAULT, 4));
    this.firstSub.show();
  }

  /**
   * Get points from all SubLegs
   */
  Leg.prototype.getPoints = function() {
    var points = [];
    points.push(this.startAnchor.marker.getLatLng());
    var leg = this.startAnchor.toLeg;
    while (leg) {
      var polyline = leg.polyline;
      var count = polyline.getVertexCount();
      for( j = 1; j < count; j++) {
        points.push(polyline.getVertex(j));
      }
      if (leg.toAnchor) {
        leg = leg.toAnchor.toLeg;
      } else {
        break;
      }
    }
    return points;
  }

  /**
   * Callback of getGDirections
   * @param {GPolyline} polyline Feed polyline to be set to the leg
   */
  Leg.prototype.onDirPolylineLoaded = function(polyline) {
    if (!polyline) {
      alert('GDirections failed!');
      return;
    }
    var color = Leg.Color.MODIFIED;
    this.hide();
    this.setPolyline(polyline, color);
    this.show();
    this.modified = true;
    this.triggerEvent('reload');
    this.triggerEvent('dragend');
  };

  /**
   * Set Leg color
   * @param {String} color RGB string of color
   */
  Leg.prototype.setColor = function(color) {
    var leg = this.startAnchor.toLeg;
    while(leg) {
      leg.setColor(color);
      if (leg.toAnchor == this.endAnchor) {
        return;
      }
      leg = leg.toAnchor.toLeg;
    }
  }

  /**
   * Show Leg
   */
  Leg.prototype.show = function() {
    var anchor = this.startAnchor;
    var leg = anchor.toLeg;
    while(leg) {
      leg.show();
      if (leg.toAnchor == this.endAnchor) {
        return;
      }
      if (leg.toAnchor) {
        anchor = leg.toAnchor;
        anchor.show()
        leg = anchor.toLeg;
      } else {
        break;
      }
    }
  };

  /**
   * Hide Leg
   */
  Leg.prototype.hide = function() {
    var anchor = this.startAnchor;
    var leg = anchor.toLeg;
    while(leg) {
      leg.hide();
      if (leg.toAnchor) {
        anchor = leg.toAnchor;
        anchor.hide();
        leg = anchor.toLeg;
      } else {
        break;
      }
    }
  };

  /**
   * Clone a new polyline from the leg
   * @return {GPolyline} Cloned polyline
   */
  Leg.prototype.clonePolyline = function() {
    var color = this.color || Leg.Color.DEFAULT;
    return new GPolyline(this.getPoints(), color, 4);
  }

  /**
   * Clone a new Leg from the Leg
   * @return {Leg} Cloned Leg
   */
  Leg.prototype.clone = function() {
    var leg = new Leg();
    leg.fromStop = this.fromStop;
    leg.toStop = this.toStop;
    leg.startAnchor = this.startAnchor;
    leg.endAnchor = this.endAnchor;
    this.firstSub = this.firstSub;
    var poly = this.clonePolyline();
    leg.setPolyline(poly);
    return leg;
  }

  /**
   * Nearest distance from point
   * @param {GLatLng} point Position of the Point
   */
  Leg.prototype.nearestDistance = function(point) {
    var shortDist = INFINITY;
    if (this.startAnchor.toLeg.toAnchor != this.endAnchor) {
      this.unify();
    }
    var poly = this.polyline;
    var count = poly.getVertexCount();
    var dist = 0.0;
    for (var i = 0; i < count; i++) {
      dist = point.distanceFrom(poly.getVertex(i));
      if (dist < shortDist) {
        shortDist = dist;
      }
    }
    if (count == 2) {
      var from = poly.getVertex(0);
      var to = poly.getVertex(1);
      var midPoint = new GLatLng((from.lat() + to.lat()) / 2,
          (from.lng() + to.lng()) /2 );
      dist = point.distanceFrom(midPoint);
      if (dist < shortDist) {
        shortDist = dist;
      }
    }
    return shortDist;
  };

  /**
   * SubLeg is a class representing the sub Leg with a Leg
   * @constructor
   */
  function SubLeg() {
    Base.call(this);
  }

  /**
   * Set polyline to the SubLeg with color
   * @param {GPolyline} polyline polyline to be set to Leg
   * @param {String} color RGB string of color
   */
  SubLeg.prototype.setPolyline = function(polyline, color) {
    if (this.polyline) {
      map.removeOverlay(this.polyline);
    }

    var count = polyline.getVertexCount();
    var latlngs = [];
    for (var i = 0; i < count; i++) {
      latlngs.push(polyline.getVertex(i));
    }
    this.polyline = new GPolyline(latlngs, Leg.Color.DEFAULT, 4);
    if (color) this.setColor(color);
    this.bindEvents_();
  };

  /**
   * Set Leg color
   * @param {String} color RGB string of color
   */
  SubLeg.prototype.setColor = function(color) {
    this.color = color;
    if (this.polyline) {
      this.polyline.setStrokeStyle({
        color: color
      });
    }
  };

  /**
   * Create polyline from points and color
   * @param {Array} points points to be converted to GPolyline
   * @param {String} color RGB string of color
   */
  SubLeg.prototype.setPoints = function(points, color) {
    var latlngs = [];
    if (!points || !points.length) {
      latlngs = [this.fromAnchor.marker.getLatLng(), this.toAnchor.marker.getLatLng()];
    } else {
      for (var i = 0; i < points.length; i++) {
        latlngs.push(new GLatLng(points[i].lat, points[i].lng));
      }
    }
    color = color || Leg.Color.DEFAULT;
    this.polyline = new GPolyline(latlngs, color, 4);
    this.bindEvents_();
  };

  /**
   * Bind events' handlers to this.polyline
   */
  SubLeg.prototype.bindEvents_ = function() {
    GEvent.bind(this.polyline, 'click', this, this.onClick);
    GEvent.bind(this.polyline, 'mousemove', this, this.onMouseOver);
    GEvent.bind(this.polyline, 'mouseout', this, this.onMouseOut);
  };

  /**
   * Click event handler
   */
  SubLeg.prototype.onClick = function() {
    if (!legEditable)
      return;
    if (Leg.dragging)
      return;
    var node = $C('div');
    var p = $C('p');
    var a = $C('a');
    a.href = 'javascript:void(0);';
    a.innerHTML = 'Get driving direction';
    p.appendChild(a);
    node.appendChild(p);
    GEvent.bindDom(a, 'click', this, this.onGetGDirections);
    p = $C('p');
    var directLine = $C('a');
    directLine.href = 'javascript:void(0);';
    directLine.innerHTML = 'Direct Leg';
    p.appendChild(directLine);
    node.appendChild(p);
    GEvent.bindDom(directLine, 'click', this, this.directLink);

    var self = this;
    map.openInfoWindow(mousePos, node);
  };

  /**
   * Transform accordingly by the user setting
   */
  SubLeg.prototype.directLink = function() {
    map.closeInfoWindow();
    map.removeOverlay(this.polyline);
    this.polyline = new GPolyline(
      [this.fromAnchor.marker.getLatLng(), this.toAnchor.marker.getLatLng()],
      Leg.Color.MODIFIED, 4);
    this.bindEvents_();
    map.addOverlay(this.polyline);
  }

  /**
   * Handler for ReGDirections
   */
  SubLeg.prototype.onGetGDirections = function() {
    map.closeInfoWindow();
    this.getGDirections();
  };

  /**
   * Handler for mouse over
   */
  SubLeg.prototype.onMouseOver = function() {
    if (!legEditable || Leg.dragging || !this.fromAnchor || !this.toAnchor) {
      return;
    }
    this.showDragMarker(mousePos);
  };

  /**
   * Handler for mouse out
   */
  SubLeg.prototype.onMouseOut = function() {
    if (!legEditable || Leg.dragging) return;
    this.hideDragMarker();
  };

  /**
   * Initialize dragMarker, reset old infomation
   * @param {GLatLng} point Position of dragMarker to be shown
   */
  SubLeg.prototype.initDragMarker = function(point) {
    var self = this;
    if (!Leg.dragMarker) {
      var icon = new GIcon();
      icon.iconSize = new GSize(8, 8);
      icon.iconAnchor = new GPoint(4, 4);
      icon.infoWindowAnchor = new GPoint(4, 0);
      var redIcon = new GIcon(icon, '/file/mm_20_red.png', null, '');

      Leg.dragMarker = new GMarker(point, {
        icon: redIcon,
        draggable: true,
        bouncy: false
      });
    }
    Leg.dragMarker.setLatLng(point);
    if (Leg.dragMarkerStartHandler) {
      GEvent.removeListener(Leg.dragMarkerStartHandler);
    }
    if (Leg.dragMarkerEndHandler) {
      GEvent.removeListener(Leg.dragMarkerEndHandler);
    }
    Leg.dragMarkerStartHandler = GEvent.addListener(Leg.dragMarker,
      'dragstart', function() {
        Leg.dragging = true;
      });
    Leg.dragMarkerEndHandler = GEvent.addListener(Leg.dragMarker, 'dragend',
      function(point) {
        Leg.dragging = false;
        var newLeg = new SubLeg();
        var anchor = new Anchor();
        anchor.setLegs(self, newLeg);
        anchor.setPos(point);

        newLeg.toAnchor = self.toAnchor;
        newLeg.fromAnchor = anchor;
        if (!newLeg.toAnchor || !newLeg.fromAnchor) {
          GLog.write('Anchor not finised');
        }
        newLeg.leg = self.leg;
        self.toAnchor = anchor
        self.hideDragMarker();
        anchor.show();
        self.getGDirections(function() {
          newLeg.getGDirections();
          Shape.recordModify(Trip.current.shape);
        });
      });
  };

  /**
   * Get the directions of this Leg, with UI changes
   */
  SubLeg.prototype.getGDirections = function(callback) {
    var self = this;
    if (!this.fromAnchor) {
      return;
    }
    if (!this.toAnchor) {
      return;
    }
    var start = this.fromAnchor.marker.getLatLng();
    var end = this.toAnchor.marker.getLatLng();
    var query = 'from: ' + getPoint(start) + ' to: ' + getPoint(end);
    getDirPolyline(query, this, function(polyline) {
        this.onDirPolylineLoaded(polyline)
        if (callback) callback();
    });
  };

  /**
   * Callback of getGDirections
   * @param {GPolyline} polyline Feed polyline to be set to the leg
   */
  SubLeg.prototype.onDirPolylineLoaded = function(polyline) {
    if (!polyline) {
      alert('GDirections failed!');
      return;
    }
    var color = Leg.Color.MODIFIED;
    this.setPolyline(polyline, color);
    this.show();
  };

  /**
   * Show drag marker for the leg
   * @param {GLatLng} point Position of dragMarker to be shown
   */
  SubLeg.prototype.showDragMarker = function(point) {
    var shortDist = INFINITY;
    var idx = 0;
    var count = this.polyline.getVertexCount();
    var pos = null;

    if (count == 2) {
      var from = this.fromAnchor.marker.getLatLng();
      var to = this.toAnchor.marker.getLatLng();
      var lat = (from.lat() + to.lat()) / 2;
      var lng = (from.lng() + to.lng()) / 2;
      pos = new GLatLng(lat, lng);

    } else if (count > 2) {
      var poly = this.polyline;
      for (var i = 1; i < count - 1; i++) {
        var dist = point.distanceFrom(poly.getVertex(i));
        if (dist < shortDist) {
          idx = i;
          shortDist = dist;
        }
      }
      pos = poly.getVertex(idx);
    } else {
      GLog.write('not a polyline');
      return;
    }
    this.initDragMarker(pos);
    map.addOverlay(Leg.dragMarker);
  };

  /**
   * Hide drag marker
   */
  SubLeg.prototype.hideDragMarker = function() {
    if (Leg.dragMarker) {
      map.removeOverlay(Leg.dragMarker);
    }
  };

  SubLeg.prototype.show = function() {
    if (this.polyline) {
      map.addOverlay(this.polyline);
    }
  };

  SubLeg.prototype.hide = function() {
    if (this.polyline) {
      map.removeOverlay(this.polyline);
    }
  };

  /**
   * Anchor is a tool for Legs to be directed by multiple points
   * @param {Leg} leg Leg will have the Anchors
   * @constructor
   */
  function Anchor() {
    Base.call(this);
    /**
     * Anchor point
     * @type {GMarker}
     */
    this.marker = null;
    this.fromLeg = null;
    this.toLeg = null;
    this.isHidden = false;
  }

  Anchor.prototype = new Base();

  Anchor.prototype.setLegs = function(fromLeg, toLeg) {
    this.fromLeg = fromLeg;
    this.toLeg = toLeg;
  }

  /**
   * Set Anchor position points
   * @param {GLatLng} point The location of the anchor
   */
  Anchor.prototype.setPos = function(point) {
    if (!this.marker) {
      var icon = new GIcon();
      icon.iconSize = new GSize(9, 18);
      icon.shadowSize = new GSize(0, 0);
      icon.iconAnchor = new GPoint(4, 18);
      icon.infoWindowAnchor = new GPoint(0, 0);
      icon.image = '/file/pin.png';
      var markerOpts = {};
      markerOpts.draggable = true;
      markerOpts.icon = icon;
      this.marker = new GMarker(point, markerOpts);
      GEvent.bind(this.marker, 'dragend', this, this.onDragEnd);
      GEvent.bind(this.marker, 'click', this, this.onClick);
    } else {
      this.marker.setLatLng(point);
    }
  }

  /**
   * Remove the anchor from the leg
   */
  Anchor.prototype.remove = function() {
    map.closeInfoWindow();
    this.hide();
    this.fromLeg.toAnchor = this.toLeg.toAnchor;
    this.toLeg.toAnchor.fromLeg = this.fromLeg;
    this.fromLeg.getGDirections();
    this.toLeg.hide();
  }

  /**
   * Show the anchor
   */
  Anchor.prototype.show = function() {
    if (this.marker && !this.isHidden) {
      map.addOverlay(this.marker);
    }
  }

  /**
   * Show the anchor
   */
  Anchor.prototype.hide = function() {
    if (this.marker) {
      map.removeOverlay(this.marker);
    }
  }

  /**
   * Handler for dragend event
   */
  Anchor.prototype.onDragEnd = function() {
    var self = this;
    if (this.fromLeg) {
      this.fromLeg.getGDirections(function() {
        if (self.toLeg) {
          self.toLeg.getGDirections();
        }
        Shape.recordModify(Trip.current.shape);
      });
      return;
    }
    if (this.toLeg) {
      this.toLeg.getGDirections();
      Shape.recordModify(Trip.current.shape);
    }
  }

  /**
   * Handler for click event
   */
  Anchor.prototype.onClick = function() {
    if (this.isHidden) return;
    var node = $C('input');
    node.type = 'button';
    node.value = 'Delete';
    var self = this;
    GEvent.addDomListener(node, 'click', function() {
      self.remove();
    });
    var p = $C('p');
    p.appendChild(node);
    this.marker.openInfoWindow(p);
  }

  /**
   * Stop is a class representing a GTFS stop.
   * @constructor
   */
  function Stop() {
    Base.call(this);
    /**
     * stop_id
     * @type {String}
     */
    this.id = '';

    /**
     * stop_name
     * @type {String}
     */
    this.name = '';

    /**
     * stop_lat
     * @type {Number}
     */
    this.lat = 0;

    /**
     * stop_lng
     * @type {Number}
     */
    this.lng = 0;

    /**
     * stop_desc
     * @type {String}
     */
    this.desc = '';

    /**
     * zone_id
     * @type {String}
     */
    this.zoneId = '';

    /**
     * stop_url
     * @type {String}
     */
    this.url = '';

    /**
     * stop_code
     * @type {String}
     */
    this.code = '';

    /**
     * location_type
     * @type {Number}
     */
    this.locationType = 0;

    /**
     * parent_station
     * @type {String}
     */
    this.parentStation = '';

    /**
     * Is parent Trip object selected
     * @type {Boolean}
     */
    this.isTripSelected = false;

    /**
     * Trips info of the Stop object
     * @type {Array}
     */
    this.tripInfo = null;
  }

  Stop.prototype = new Base();


  /**
   * Icons for stops
   * @type {Object}
   */
  Stop.icons = null;

  /**
   * Handler for all stop actions
   * @param {Object} act Infomation of an action
   * @param {Function} cb Callback function
   */
  Stop.handleAct = function(act, cb) {
    var stop = act.obj.stop;
    switch (act.act) {
     case 'add':
        stop.saveForAdd(cb, act);
        break;
     case 'copy':
        stop.saveForCopy(cb, act);
        break;
      case 'modify':
        stop.saveForModify(cb, act);
        break;
    }
  }

  /**
   * Confirm a Stop object to be created and return the stop by ID
   * @param {Object} stopInfo Info of a Stop object
   * @return {Stop} Stop object
   */
  Stop.confirmAndGetStop = function(stopInfo) {
    var stop = stopsFetched[stopInfo.id];
    if (!stop) {
      stop = new Stop();
      for (attr in stopInfo) {
        stop[attr] = stopInfo[attr];
      }
      stop.createMarker();
      stopsFetched[stop.id] = stop;
    }
    return stop;
  };

  /**
   * Icons initialization
   */
  Stop.initIcons = function() {
    Stop.icons = {};

    var normal = this.createIcon();
    normal.image = '/file/mm_20_yellow.png';
    normal.shadow = '/file/mm_20_shadow.png';
    Stop.icons['normal'] = normal;

    var background = this.createIcon();
    background.image = '/file/mm_20_blue_trans.png';
    background.shadow = '/file/mm_20_shadow_trans.png';

    Stop.icons['background'] = background;

    var station = this.createIcon();
    station.image = '/file/mm_20_red_trans.png';
    station.shadow = '/file/mm_20_shadow_trans.png';

    Stop.icons['station'] = station;
  };

  /**
   * Marker for adding a stop
   * @type {GMarker}
   */
  Stop.addStopCursor = null;

  Stop.initAddStopCursor = function() {
    var markerOpts = {};
    markerOpts.icon = Stop.icons.normal;
    Stop.addStopCursor = new GMarker(new GLatLng(0.0, 0.0), markerOpts);
  }

  /**
   * Set Stop status
   * @param {Boolean} status If the trip of the stop is current or not
   */
  Stop.prototype.setStatus = function(status) {
    this.isTripSelected = status;
    this.clear();
  };

  /**
   * Toggle labeledMarker_ from editable to not or reversely.
   * @param {Boolean} editable Editing option for labled marker
   */
  Stop.prototype.setEditable = function(editable) {
    if (editable) {
      this.labeledMarker_.enableDragging();
    } else {
      this.labeledMarker_.disableDragging();
    }
  };

  /**
   * Create all markers for the Stop object
   */
  Stop.prototype.createMarker = function() {
    this.latlng = new GLatLng(this.lat, this.lng);
    this.createBackgroundMarker();
    this.createStationMarker();
  };

  /**
   * Handler for Stop moved
   */
  Stop.prototype.onDragEnd = function() {
    this.latlng = this.labeledMarker_.getLatLng();
    this.backgroundMarker_.setLatLng(this.latlng);
    this.stationMarker_.setLatLng(this.latlng);
    
    if (withLegReload)
      this.triggerEvent('dragend', this.latlng);
    
    if (!actionFind('stop', 'modify', 'stop', this)) {
      actionList.push({type: 'stop', act: 'modify', obj: {stop: this}});
      actionList.push({type: 'shape', act: 'modify',obj: {shape:
        Trip.current.shape}});
    }

    this.modified = true;
  };

  /**
   * Handler for Stop selection
   */
  Stop.prototype.onSelect = function() {
    if (stopEditable && this.isTripSelected) {
      this.showEditInfo();
      return;
    }
    if (!this.tripInfo) {
      this.tripInfo = [];
    }
    this.showInfo();
  // No longer get cross stop information for efficiency reason.
  /*
    if (this.inAsync) return;
    this.inAsync = true;
    this.fetcher.getStopInfo(this.id, this, function(data) {
      //data format in (time, trip(id, name, service_id), timepoint)
      this.tripInfo = data;
      this.showInfo();
      this.inAsync = false;
    });
    */
  };

  /**
   * Remove stops from list
   */
  Stop.prototype.removeStopFromList = function(list) {
    for (var i = 0, stop; i < list.length; i++) {
      stop = list[i];
      if (stop == this) {
        list.splice(i, 1);
        break;
      }
    }
  }

  /**
   * Handler for stop saved
   */
  Stop.prototype.onSaved = function() {
    this.inAsync = false;
    this.modified = false;
  };

  /**
   * Handler on stop modified
   * @param {Object} data Data from server
   * @param {Object} act Infomation of the act
   * @param {Function} cb Callback function
   */
  Stop.prototype.onModified = function(data, act, cb) {
    this.onSaved();
    cb();
  }

  /**
   * Handler on stop copied
   * @param {Object} data Data from server
   * @param {Object} act Infomation of the act
   * @param {Function} cb Callback function
   */
  Stop.prototype.onCopied = function(data, act, cb) {
    data = data[0];
    this.id = data.id + '';
    this.parentStation = data.parentStation;
    this.onSaved();
    var stoptime = act.obj.stoptime;
    var trip = stoptime.trip;
    trip.onStopCopied(stoptime, cb)
  };

  /**
   * Handler on stop added
   * @param {Object} data Data from server
   * @param {Object} act Infomation of the act
   * @param {Function} cb Callback function
   */
  Stop.prototype.onAdded = function(data, act, cb) {
    data = data[0];
    this.id = data.id + '';
    this.onSaved();
    var stoptime = act.obj.stoptime;
    var sequence = act.obj.sequence;
    var trip = stoptime.trip;
    trip.onStopAdded(stoptime, cb, sequence)
  };


  /**
   * Base function on update stop
   * @param {Object} param Key value object
   * @param {Function} mbcb Member callback function
   * @param {Function} callback Async callback
   * @param {Object} act Infomation of the act
   *
   */
  Stop.prototype.updateStop = function(param, mbcb, callback, act) {
    param['name'] = this.name;
    param['latlng'] = this.latlng.lat() + ',' + this.latlng.lng();
    if (this.inAsync) return;
    this.inAsync = true;
    this.fetcher.updateStop(param, this, function(data) {
      mbcb.call(this, data, act, callback);
    });
  }

  /**
   * On starting to copy
   * @param {Function} callback Callback function
   * @param {Object} act Infomation of the act
   */
  Stop.prototype.saveForCopy = function(callback, act) {
    var parentId = this.getRealId();
    var param = {};
    param['act'] = 'copy';
    param['parentId'] = parentId;
    this.updateStop(param, this.onCopied, callback, act);
  };

  /**
   * On starting to add
   * @param {Function} callback Callback function
   * @param {Object} act Infomation of the act
   */
  Stop.prototype.saveForAdd = function(callback, act) {
    var param = {};
    param['act'] = 'add';
    this.updateStop(param, this.onAdded, callback, act);
  };

  /**
   * On modify
   * @param {Function} callback Callback function
   * @param {Object} act Infomation of the act
   */
  Stop.prototype.saveForModify = function(callback, act) {
    var param = {};
    param['act'] = 'save';
    param['stopId'] = this.id;
    this.updateStop(param, this.onModified, callback, act);
  };

  /**
   * Saving Stop info to the server side
   * @param {Function} callback Callback function on task finished
   */
  Stop.prototype.save = function(callback) {
  };

  /**
   * Create element which is used for deleting the stop
   * @return {HTMLElement} The element for deleting the stop
   */
  Stop.prototype.createDeleteNode = function() {
    var line = $C('span');
    var copyBtn = $C('input');
    copyBtn.type = 'button';
    copyBtn.value = 'Delete';

    line.appendChild(copyBtn);
    GEvent.bindDom(copyBtn, 'click', this, this.onDeleteNodeClick);
    return line;
  };

  /**
   * Create element which is used for copying the stop
   * @return {HTMLElement} The element for copying the stop
   */
  Stop.prototype.createCopyNode = function() {
    var line = $C('span');
    var copyBtn = $C('input');
    copyBtn.type = 'button';
    copyBtn.value = 'Split';

    line.appendChild(copyBtn);
    GEvent.bindDom(copyBtn, 'click', this, this.onCopyNodeClick);
    return line;
  };

  /**
   * Handler for copy node click
   */
  Stop.prototype.onCopyNodeClick = function() {
    if (confirm('Sure to copy stop? New stop will be created.')) {
      var stoptime = Trip.current.getStopTimeByStop(this);
      stoptime.onCopy();
    }
  };

  /**
   * Handler for delete node click
   */
  Stop.prototype.onDeleteNodeClick = function() {
    if(confirm('Sure to Delete stop? It is not revertable.')) {
      Trip.current.onDeleteStop(this, true);
    }
  };


  Stop.COPY_STOP_ID_PREFIX = 'copy';
  Stop.ADD_STOP_ID_PREFIX = 'add';


  /**
   * Determine if the stop id is an add id
   * @return {Boolean}
   */
  Stop.prototype.isAddId = function(id) {
    id = id || this.id;
    return (id.indexOf(Stop.ADD_STOP_ID_PREFIX) != -1)
  }

  /**
   * Determine if the stop id is an copy id
   * @return {Boolean}
   */
  Stop.prototype.isCopyId = function(id) {
    id = id || this.id;
    return (id.indexOf(Stop.COPY_STOP_ID_PREFIX) != -1)
  }

  /**
   * Determine if the stop id is a temporary id
   * @return {Boolean}
   */
  Stop.prototype.isTempId = function(id) {
    id = id || this.id;
    return (id.indexOf(Stop.ADD_STOP_ID_PREFIX) != -1) ||
      (id.indexOf(Stop.COPY_STOP_ID_PREFIX) != -1)
  }

  /**
   * Make a temporary add id for the stop
   * @return {String}
   */
  Stop.generateAddId = function() {
    return Stop.ADD_STOP_ID_PREFIX + new Date().getTime();
  }

  /**
   * Make a temporary add id for the stop
   * @return {String}
   */
  Stop.generateCopyId = function(id) {
    id = id || this.id;
    if (this.isAddId(id)) return null;
    if (this.isCopyId(id)) return id;
    return Stop.COPY_STOP_ID_PREFIX + id;
  }


  /**
   * Make a temporary id for the stop
   * @return {Boolean}
   */
  Stop.prototype.makeTempId = function(id) {
    id = id || this.id;
    if (this.isAddId(id)) return null;
    if (this.isCopyId(id)) return id;
    return Stop.COPY_STOP_ID_PREFIX + id;
  }

  /**
   * Get the real id
   * @return {Boolean}
   */
  Stop.prototype.getRealId = function(id) {
    id = id || this.id;
    if (!this.isTempId(id)) return id;
    var len = Stop.COPY_STOP_ID_PREFIX.length;
    return id.substring(len, id.length);
  }

  /**
   * Show editing window for Stop
   */
  Stop.prototype.showEditInfo = function() {
    var sequence = Trip.current.getStopSequence(this);
    var div = $C('div');
    var mod = this.modified ? '<span style="color:red">*</span>' : '';
    var title = $C('h5');
    title.innerHTML = this.name + '(' + this.id + ') seq:' + sequence + mod;
    div.appendChild(title);

    var pos = $C('p');
    pos.innerHTML = '(' + this.latlng.lat() + ', ' + this.latlng.lng() + ')';
    div.appendChild(pos);

    var nameNode = this.createNameEditingNode(title);
    div.appendChild(nameNode);

    if (!this.isTempId()) {
      var copyNode = this.createCopyNode();
      div.appendChild(copyNode);
      var deleteNode = this.createDeleteNode();
      div.appendChild(deleteNode);
    }

    this.labeledMarker_.openInfoWindow(div);
  };

  /**
   * Create element which is used for editing the stop name.
   * @param {HTMLElement} title Title of the stop info
   */
  Stop.prototype.createNameEditingNode = function(title) {
    var line = $C('p');
    var stopNameInput = $C('input');
    stopNameInput.type = 'text';
    stopNameInput.value = '';
    line.appendChild(stopNameInput);
    var updateNameBtn = $C('input');
    updateNameBtn.type = 'button';
    updateNameBtn.value = 'Rename';
    line.appendChild(updateNameBtn);
    line.appendChild($C('br'));

    var self = this;

    var onUpdateName = function() {
      if (stopNameInput.value) {
        self.modified = true;
        var mod = self.modified ? '<span style="color:red">*</span>' : '';
        self.name = stopNameInput.value;
        title.innerHTML = self.name + '(' + self.id + ')' + mod;
        if (!actionFind('stop', 'modify', 'stop', self)) {
          actionList.push({type: 'stop', act: 'modify', obj: {stop: self}});
        }
      }
    };
    GEvent.addDomListener(updateNameBtn, 'click', onUpdateName);
    return line;
  };


  /**
   * Show Stop info fetched
   */
  Stop.prototype.showInfo = function() {
    var sequence = Trip.current.getStopSequence(this);
    var stopInfo = $C('div');
    stopInfo.innerHTML = '<b>' + this.name + '</b> (' + this.id + ') seq:' +
      sequence + '<br>(' +
      this.latlng.lat() + ', ' + this.latlng.lng() + ')<br>';

    var tripTable = $C('table');

    var header = $C('tr');
    var ths = ['service_id', 'time', 'name'];
    for (var i = 0, name; name = ths[i]; i++) {
      var th = $C('th');
      th.innerHTML = name;
      header.appendChild(th);
    }
    tripTable.appendChild(header);

    var tripInfo = this.tripInfo;
    for (i = 0; i < tripInfo.length; ++i) {
      var time = tripInfo[i][0];
      var tripId = tripInfo[i][1][0];
      var tripName = tripInfo[i][1][1];
      var serviceId = tripInfo[i][1][2];
      var timepoint = tripInfo[i][2];

      var row = $C('tr');
      row.tripId = tripId;
      GEvent.bindDom(row, 'click', row, function() {
        var trip = tripsFetched[this.tripId];
        if (trip) {
          trip.onSelect();
        }
      });

      var tds = [$C('td'), $C('td'), $C('td')];
      var htmls = [serviceId, (timepoint ? '' : '~') + time, tripName];
      for (i = 0, td; td = tds[i]; i++) {
        td.innerHTML = htmls[i];
        row.appendChild(td);
      }
      tripTable.appendChild(row);
    }

    var div = $C('div');
    div.appendChild(stopInfo);

    // Currently table is ignored
    //div.appendChild(tripTable);

    map.openInfoWindow(this.latlng, div);
  };

  /**
   * Create a labeled marker for the Stop object
   * @param {String} arrivalTime String of time
   */
  Stop.prototype.createLabeledMarker = function(arrivalTime) {
    var markerOpts = {};
    markerOpts.icon = Stop.icons.normal;
    var image = Stop.icons.normal.image;
    if (arrivalTime) {
      markerOpts.labelText = arrivalTime;
    }
    markerOpts.draggable = true;
    markerOpts.labelClass = 'tooltip';
    markerOpts.labelOffset = new GSize(6, - 20);
    var marker = new LabeledMarker(this.latlng, markerOpts);
    if (!stopEditable) {
      marker.disableDragging();
    }
    this.labeledMarker_ = marker;
    GEvent.bind(marker, 'click', this, this.onSelect);
    GEvent.bind(marker, 'dragend', this, this.onDragEnd);
  };

  /**
   * Set marker text
   * @param {String} text String of time to be set
   */
  Stop.prototype.setText = function(text) {
    if (this.labeledMarker_) this.labeledMarker_.setText(text);
  };

  /**
   * Create a background marker for the Stop object
   */
  Stop.prototype.createBackgroundMarker = function() {
    var markerOpts = {};
    markerOpts.icon = Stop.icons.background;
    var marker = new GMarker(this.latlng, markerOpts);
    this.backgroundMarker_ = marker;
    GEvent.bind(marker, 'click', this, this.onSelect);
  };

  /**
   * Create a station marker for the Stop object
   */
  Stop.prototype.createStationMarker = function() {
    var markerOpts = {};
    markerOpts.icon = Stop.icons.station;
    var marker = new GMarker(this.latlng, markerOpts);
    this.stationMarker_ = marker;
    GEvent.bind(marker, 'click', this, this.onSelect);
  };

  /**
   * Get the GLatLng of the Stop object
   * @return {GLatLng} Stop position in GLatLng
   */
  Stop.prototype.getLatLng = function() {
    if (!this.latlng) {
      this.latlng = new GLatLng(this.lat, this.lng);
    }
    return this.latlng;
  };

  /**
   * Alias to member function clear
   */
  Stop.prototype.hide = function() {
    this.clear();
  };

  /**
   * Clear overlays of this obejct
   */
  Stop.prototype.clear = function() {
    if (this.labeledMarker_) {
      map.removeOverlay(this.labeledMarker_);
    }
    map.removeOverlay(this.backgroundMarker_);
    map.removeOverlay(this.stationMarker_);
  };

  /**
   * Add overlay of this obejct to
   */
  Stop.prototype.show = function() {
    this.clear();
    if (this.isTripSelected) {
      map.addOverlay(this.labeledMarker_);
      return;
    } else if (this.locationType == 1) {
      map.addOverlay(this.stationMarker_);
    } else {
      map.addOverlay(this.backgroundMarker_);
    }
    if (this.parentStation) {
      station = stopsFetched[this.parentStation];
      if (station) {
        station.show();
      }
    }
  };

  /**
   * Create a new GIcon
   * @return {GIcon} Object of GIcon
   */
  Stop.createIcon = function() {
    var icon = new GIcon();
    icon.iconSize = new GSize(12, 20);
    icon.shadowSize = new GSize(22, 20);
    icon.iconAnchor = new GPoint(6, 20);
    icon.infoWindowAnchor = new GPoint(5, 1);
    return icon;
  };

  /**
   * StopTime is a class representing a GTFS stoptime item.
   * @constructor
   */
  function StopTime() {
    Base.call(this);
    /**
     * trip_id
     * @type {String}
     */
    this.tripId = '';

    /**
     * arrival_time
     * @type {String}
     */
    this.arrivalTime = '';

    /**
     * departure_time
     * @type {String}
     */
    this.departureTime = '';

    /**
     * stop_id
     * @type {String}
     */
    this.stopId = '';

    /**
     * stop_sequence
     * @type {Number}
     */
    this.stopSequence = 0;

    /**
     * stop_headsign
     * @type {String}
     */
    this.stopHeadsign = '';

    /**
     * pickup_type
     * @type {Number}
     */
    this.pickupType = 0;

    /**
     * drop_off_type
     * @type {Number}
     */
    this.dropOffType = 0;

    /**
     * shape_dist_traveled
     * @type {Number}
     */
    this.shapeDistTraveled = 0;
  }

  StopTime.prototype = new Base();

  /**
   * Handler for stop copy
   */
  StopTime.prototype.onCopy = function() {
    var stop = this.stop;

    var newStop = new Stop();
    newStop.name = stop.name;
    newStop.lat = stop.lat;
    newStop.lng = stop.lng;
    newStop.desc = stop.desc;
    newStop.zoneId = stop.zoneId;
    newStop.url = stop.url;
    newStop.code = stop.code;
    newStop.locationType = 0;
    var id = stop.makeTempId();
    newStop.id = id;
    newStop.latlng = new GLatLng(newStop.lat, newStop.lng);
    if (!stop.parentStation) {
      stop.locationType = 1;
      newStop.parentStation = stop.id;
    } else {
      newStop.parentStation = stop.parentStation;
    }
    newStop.createMarker();
    newStop.isTripSelected = true;
    this.stop = newStop;
    this.stop.createLabeledMarker(this.arrivalTime);

    newStop.fromLeg = stop.fromLeg;
    newStop.toLeg = stop.toLeg;
    if (newStop.fromLeg) newStop.fromLeg.toStop = newStop;
    if (newStop.toLeg) newStop.toLeg.fromStop = newStop;
    actionList.push({type: 'stop', act: 'copy',
      obj: {stop: newStop, stoptime: this}});
    newStop.modified = true;
    stop.hide();
    newStop.show();
    this.trip.shape.replaceStop(stop, newStop);
    this.modified = true;
  };

  /**
   * Save stoptime info
   * @param {Function} cb Callback function
   */
  StopTime.prototype.saveChange = function(cb) {
      var param = {
        act: 'deletestoptime',
        rid: this.trip.rid,
        pid: this.trip.patternId,
        seq: this.sequence,
        sid: this.stop.id
      };
      this.fetcher.updatePattern(param, this, function(data) {
        this.onPatternStoptimeRemoved(data);
        cb();
      });
  }
  /**
   * Save stoptime info
   */
  StopTime.prototype.save = function() {
  };

  /**
   * Callback handler for stoptime removed
   * @param {Number} retCode Return value from server
   */
  StopTime.prototype.onPatternStoptimeRemoved = function(retCode) {
    if (retCode == 1) {
      var trips = this.trip.route.patterns[this.trip.patternId];
      var idx = this.trip.getStoptimeIndex(this);
      if (idx == -1) {
        return;
      }
      for (var i = 0, len = trips.length; i < len; i ++) {
        var trip = trips[i];
        if (trip && trip.stoptimes && trip.stoptimes.length) {
          trip.stoptimes.splice(idx, 1);
        }
      }
    }
  }

  /**
   * MapTool is a class controlling GMap2 for GTFS Objects.
   * @constructor
   * @param {String} id Id of HTML node
   *
   */
  function MapTool(id) {
    Base.call(this);
    this.id_ = id;
    this.map_ = null;
    this.createMap($(id));
    this.setBounds(minLatLng, maxLatLng);
    this.init();
  }

  MapTool.prototype = new Base();

  /**
   * Create GMap2
   * @param {Object} node Node for GMap2
   */
  MapTool.prototype.createMap = function(node) {
    this.sizeRouteList();
    map = new GMap2(node);
    map.addControl(new GLargeMapControl());
    map.addControl(new GMapTypeControl());
    map.addControl(new GOverviewMapControl());
    map.enableScrollWheelZoom();
    map.enableDoubleClickZoom();
    this.map_ = map;
  };

  /**
   * Set default bounds
   * @param {GLatLng} min Minimum GLatLng of bounds
   * @param {GLatLng} max Maximum GLatLng of bounds
   */
  MapTool.prototype.setBounds = function(min, max) {
    var maxBounds = new GLatLngBounds(min, max);
    var bounds = this.map_.getBoundsZoomLevel(maxBounds);
    this.map_.setCenter(maxBounds.getCenter(), bounds);
  };

  /**
   * Map event handler on zoom end
   */
  MapTool.prototype.onZoomEnd = function() {
  };

  /**
   * Map event handler on move end
   */
  MapTool.prototype.onMoveEnd = function() {
    if (this.moveEndTimer_) {
      clearTimeout(this.moveEndTimer_);
    }
    var self = this;
    this.moveEndTimer_ =
    setTimeout(function() {
      self.fetchStopsInBounds(self.map_.getBounds());
      self.moveEndTimer_ = null;
    }, 500);
  };

  MapTool.prototype.fetchStopsInBounds = function(bounds) {
    this.fetcher.getStopsInBounds(bounds, this, this.onFetchStopsInBounds);
  };

  MapTool.prototype.onFetchStopsInBounds = function(data) {
    for (var i = 0, stopInfo; stopInfo = data[i]; i++) {
      var stop = Stop.confirmAndGetStop(stopInfo);
      if (!stop.isTripSelected) {
        showStopsInBounds ? stop.show() : stop.clear();
      }
    }
  };

  /**
   * Map initialization
   */
  MapTool.prototype.init = function() {
    this.initEvent();
  };

  /**
   * Event initialization
   */
  MapTool.prototype.initEvent = function() {
    GEvent.bind(this.map_, 'moveend', this, this.onMoveEnd);
    GEvent.bind(this.map_, 'zoomend', this, this.onZoomEnd);
    GEvent.bindDom(window, 'resize', this, this.sizeRouteList);
    GEvent.bindDom($('tripSearch'), 'click', this, this.onTripSearch);
    GEvent.bindDom($('stopSearch'), 'click', this, this.onStopSearch);
    this.onMoveEnd(); // Pretend we just moved to current center
  };

  /**
   * Select trip if the trip is initialized
   */
  MapTool.prototype.onTripSearch = function() {
    var trip = tripsFetched[$('tripTextSearchInput').value];
    if (trip) {
      trip.onSelect();
    }
  };

  /**
   * Select stop if the stop is initialized
   */
  MapTool.prototype.onStopSearch = function() {
    var stop = stopsFetched[$('stopTextSearchInput').value];
    if (stop) {
      stop.onSelect();
    }
  };

  /**
   * Resize the route list on window size change
   */
  MapTool.prototype.sizeRouteList = function() {
    var bottombarHeight = 0;
    var bottombarDiv = $('bottombar');
    if (bottombarDiv.style.display != 'none') {
      bottombarHeight = $('bottombar').offsetHeight +
        $('bottombar').style.marginTop;
    }
    var height = windowHeight() - $('topbar').offsetHeight -
      15 - bottombarHeight;
    $('content').style.height = height + 'px';
    if (this.map_) {
      // Without this displayPolyLine does not use the correct map size
      this.map_.checkResize();
    }
  };

  var errs = {};
  errs['incompatible'] = 'Browser is not supported or Google ' +
                         'Map API is not loaded!';

  /**
   * gtfs initialization function
   */
  function gtfsLoad() {
    //former loader
    var mapTool = new MapTool('map');
    Stop.initIcons();
    Stop.initAddStopCursor();
    //new gtfs loader
    var manager = new Manager();
    manager.getAgencies(function() {
      manager.showAgencies();
    });
    GEvent.addListener(map, 'mousemove', function(pos) {
      mousePos = pos;
      if (addStopMode) {
        Stop.addStopCursor.setLatLng(pos);
        if (Trip.current) {
          Trip.current.checkAddStopStatus(pos);
        }
      } else {
        if (Trip.current) {
          Trip.current.clearAddStopPolyline();
        }
      }
    });
    GEvent.addListener(map, 'click', function(object, pos, objPos) {
      pos = pos || objPos;
      if (addStopMode) {
        if (Trip.current) {
          Trip.current.onAddStop(pos);
        }
      }
    });

    GEvent.addDomListener($('generateall'), 'click', function() {
      manager.generateAllShapes(function() {
        $('gnote').innerHTML = 'All shapes are generated and saved!';
      });
    });

    GEvent.addDomListener($('saveall'), 'click', function() {
      map.closeInfoWindow();
      saveActions();
    });


    GEvent.addDomListener($('export'), 'click', function() {
      if (isExporting) {
        $('gnote').innerHTML = 'In exporting now, please be patient!';
        return;
      }
      isExporting = true;
      manager.fetcher.exportGTFSData(null, function() {
        $('gnote').innerHTML = 'GTFS data exporting finished!';
        isExporting = false;
      });
      $('gnote').innerHTML = 'Exporting GTFS file now, please be patient!';
    });
  }

  if (GBrowserIsCompatible()) {
    GEvent.addDomListener(window, 'load', gtfsLoad);
    GEvent.addDomListener(window, 'unload', GUnload);
  } else {
    throw new Error(errs['incompatible']);
  }
  gtfs.Select = Select;
}(gtfs));
